Merge pull request 'feature/cleanup-details' (#9) from feature/cleanup-details into main

Reviewed-on: https://git.walzen665.de/Walzen665/fileshare-service/pulls/9
This commit is contained in:
Max W. 2024-02-19 16:39:10 +00:00
commit 62ecc6632a
22 changed files with 453 additions and 162 deletions

View File

@ -1,21 +1,40 @@
<p>credits works!</p> <div class="flex flex-col items-center justify-center min-h-fit p-4 lg:p-28">
<div class="flex items-center justify-center mb-3">
<a href="/" class="flex items-center justify-center">
<img src="./assets/cloud-solid.svg" class="mr-3 w-24 animate-bounce ease-out" alt="SharePulse Logo" />
<span class="self-center text-4xl font-semibold whitespace-nowrap">SharePulse</span>
</a>
</div>
<h2 class="text-2xl font-semibold text-gray-800 mb-10 text-center">
Upload a file. Share the URL/link. Download the file. Simple as that.
</h2>
<div class="space-y-4 text-xl mb-20">
<div class="grid grid-cols-1 sm:grid-cols-4 items-center gap-4">
<h3 class="font-semibold text-gray-800 sm:col-span-2 md:text-right">Developer:</h3>
<p class="text-gray-800 sm:col-span-2 sm:text-left">Walzen665</p>
</div>
<div class="grid grid-cols-1 sm:grid-cols-4 items-center gap-4">
<h3 class="font-semibold text-gray-800 sm:col-span-2 md:text-right">Version:</h3>
<p class="text-gray-800 sm:col-span-2 sm:text-left">1.0.0</p>
</div>
<div class="grid grid-cols-1 sm:grid-cols-4 items-center gap-4">
<h3 class="font-semibold text-gray-800 sm:col-span-2 md:text-right">API-Status:</h3>
<p class="sm:col-span-2 sm:text-left text-green-600">online</p>
</div>
<div class="grid grid-cols-1 sm:grid-cols-4 items-center gap-4">
<h3 class=" font-semibold text-gray-800 sm:col-span-2 md:text-right">Libraries:</h3>
<p class="text-gray-800 sm:col-span-2 sm:text-left">
<a href="https://fontawesome.com/" class="text-indigo-600 hover:text-indigo-800">Fontawesome</a>,
<a href="https://tailwindcss.com/" class="text-indigo-600 hover:text-indigo-800">TailwindCSS</a>,
<a href="https://axios-http.com/" class="text-indigo-600 hover:text-indigo-800">AXIOS http</a>,
<a href="https://www.npmjs.com/package/tailwindcss-animate" class="text-indigo-600 hover:text-indigo-800">TailwindCSS Animations</a>
</p>
</div>
</div>
<h2 class="text-2xl font-semibold text-gray-800 mb-4"> <p>
Used media <span class="text-indigo-600 cursor-pointer" (click)="openPrivacyPolicyModal()">Privacy Policy</span> |
</h2> <span class="text-indigo-600 cursor-pointer" (click)="openTermsOfUseModal()">Terms of Use</span>
<ul class="list-disc pl-5 space-y-2"> </p>
<li>
<p class="font-medium text-gray-700">Icons used:</p> </div>
<a href="https://fontawesome.com/" class="text-blue-500 hover:text-blue-600 transition duration-300 ease-in-out">Fontawesome</a>
</li>
<li>
<p class="font-medium text-gray-700">HTML, CSS, JS Libraries used:</p>
<a href="https://tailwindcss.com/" class="text-blue-500 hover:text-blue-600 transition duration-300 ease-in-out">TailwindCSS</a>,
<a href="https://axios-http.com/" class="text-blue-500 hover:text-blue-600 transition duration-300 ease-in-out">AXIOS http</a>,
<a href="https://www.npmjs.com/package/tailwindcss-animate" class="text-blue-500 hover:text-blue-600 transition duration-300 ease-in-out">TailwindCSS Animations</a>
</li>
<li>
<p class="font-medium text-gray-700">App Icon by flaticon.com</p>
<a href="https://www.flaticon.com/de/autoren/najmunnahar" class="text-blue-500 hover:text-blue-600 transition duration-300 ease-in-out">NajmunNahar</a>
</li>
</ul>

View File

@ -1,12 +1,26 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import {RouterLink} from "@angular/router";
import {LegalService} from "../../service/legalService";
@Component({ @Component({
selector: 'app-credits', selector: 'app-credits',
standalone: true, standalone: true,
imports: [], imports: [
RouterLink
],
templateUrl: './credits.component.html', templateUrl: './credits.component.html',
styleUrl: './credits.component.scss' styleUrl: './credits.component.scss'
}) })
export class CreditsComponent { export class CreditsComponent {
constructor(private legalService: LegalService) {
}
openPrivacyPolicyModal() {
this.legalService.openPrivacyPolicy();
}
openTermsOfUseModal() {
this.legalService.openTermsOfUse();
}
} }

View File

@ -4,7 +4,7 @@ import {DevelopmentStore} from "../../store/DevelopmentStore";
import {FormsModule} from "@angular/forms"; import {FormsModule} from "@angular/forms";
import {DecimalPipe, NgClass, NgIf} from "@angular/common"; import {DecimalPipe, NgClass, NgIf} from "@angular/common";
import funfacts from "../../assets/funfacts"; import funfacts from "../../assets/funfacts";
import {RouterLink} from "@angular/router"; import {ActivatedRoute, RouterLink} from "@angular/router";
@Component({ @Component({
selector: 'app-download', selector: 'app-download',
@ -24,7 +24,7 @@ export class DownloadComponent {
@ViewChild('download_not_possible') download_not_possible: ElementRef<HTMLDialogElement> | undefined; @ViewChild('download_not_possible') download_not_possible: ElementRef<HTMLDialogElement> | undefined;
@ViewChild('file_not_found_modal') file_not_found_modal: ElementRef<HTMLDialogElement> | undefined; @ViewChild('file_not_found_modal') file_not_found_modal: ElementRef<HTMLDialogElement> | undefined;
inputFileId: string = "2402183"; inputFileId: string = "";
fileId: string = ""; fileId: string = "";
filePassword: string = ""; filePassword: string = "";
fileName: string = ""; fileName: string = "";
@ -34,14 +34,28 @@ export class DownloadComponent {
fileDownloadFinished: boolean = false; fileDownloadFinished: boolean = false;
waitingForPassword: boolean = false; waitingForPassword: boolean = false;
downloadProgress: number = 0; downloadProgress: number = 0;
targetUploadProgress: number = 0;
downloadDuration: string = ""; downloadDuration: string = "";
passwordWrong: boolean = false; passwordWrong: boolean = false;
funfact: string = ""; funfact: string = "";
constructor(private developmentStore: DevelopmentStore) { constructor(private developmentStore: DevelopmentStore, private activatedRoute: ActivatedRoute) {
this.funfact = funfacts[Math.floor(Math.random() * funfacts.length)]; this.funfact = funfacts[Math.floor(Math.random() * funfacts.length)];
this.activatedRoute.queryParams.subscribe(params => {
const fileId = params['fileId'];
const password = params['password'];
if(password) {
this.filePassword = password;
}
if(fileId) {
this.inputFileId = fileId;
this.requestDownload();
}
});
this.speedTest(); this.speedTest();
} }
@ -70,6 +84,11 @@ export class DownloadComponent {
return; return;
} }
else if(this.downloadInfo?.passwordProtected) { else if(this.downloadInfo?.passwordProtected) {
if(this.filePassword) {
console.log("Requesting download with password");
this.downloadFile();
return;
}
this.waitingForPassword = true; this.waitingForPassword = true;
console.log("Password protected"); console.log("Password protected");
} }
@ -137,8 +156,8 @@ export class DownloadComponent {
// Calculate the percentage of download completed // Calculate the percentage of download completed
if(progressEvent.total) { if(progressEvent.total) {
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total); const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
this.downloadProgress = percentCompleted; this.targetUploadProgress = percentCompleted;
console.log(percentCompleted + '%'); // Log the percentage or update any progress UI component this.smoothProgressUpdate();
} }
} }
}) })
@ -183,6 +202,16 @@ export class DownloadComponent {
private wrongPassword() { private wrongPassword() {
this.passwordWrong = true; this.passwordWrong = true;
} }
smoothProgressUpdate() {
if (this.downloadProgress < this.targetUploadProgress) {
this.downloadProgress += 0.01 * (this.targetUploadProgress - this.downloadProgress);
requestAnimationFrame(this.smoothProgressUpdate.bind(this));
} else if (this.downloadProgress > this.targetUploadProgress) {
// Handle overshoot
this.downloadProgress = this.targetUploadProgress;
}
}
} }
interface DownloadInfo { interface DownloadInfo {
downloadable: boolean; downloadable: boolean;

View File

@ -25,72 +25,11 @@
<p class="text-gray-600 text-center mb-4">We prioritize data security and privacy consequently. <p class="text-gray-600 text-center mb-4">We prioritize data security and privacy consequently.
Files uploaded by our clients are securely stored on our servers for a maximum duration of one day, Files uploaded by our clients are securely stored on our servers for a maximum duration of one day,
after which they are permanently deleted to guarantee the highest level of confidentiality and data protection. Read more in our after which they are permanently deleted to guarantee the highest level of confidentiality and data protection. Read more in our
<span class="text-indigo-600 cursor-pointer" (click)="openPrivacyPolicy()">Privacy Policy</span> note. <span class="text-indigo-600 cursor-pointer" (click)="openPrivacyPolicyModal()">Privacy Policy</span> note.
By using SharePulse, you agree to the <span class="text-indigo-600 cursor-pointer" (click)="openTermsOfUse()">Terms of Use</span>.</p> By using SharePulse, you agree to the <span class="text-indigo-600 cursor-pointer" (click)="openTermsOfUseModal()">Terms of Use</span>.</p>
</div> </div>
</div> </div>
<dialog #privacy_policy_modal class="modal">
<div class="modal-box w-11/12 max-w-5xl">
<h3 class="font-bold text-lg">SharePulse™ Privacy Policy</h3>
<div>
<div class="mx-auto my-6">
<ol class="list-decimal">
<li class="ml-4 mb-2 text-gray-800 font-semibold">Uploaded files are stored on a secure server.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">Files are stored for a maximum of 24 hours.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">Sharepulse tracks your IP address when uploading files.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">Sharepulse may track your IP address when downloading files.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">Sharepulse does not track you when accessing the website.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">Personal information collected by Sharepulse is used solely for data protection and is not processed.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">Sharepulse will not sell, rent, or share personal information with third parties without user consent, except as required by law.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">Users have the right to request the deletion of their data from Sharepulse's servers at any time.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">Sharepulse does not store any cookies on your device.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">Operators do not view or manipulate uploaded files.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">Users are encouraged to review the privacy policy periodically for any changes or updates.</li>
</ol>
</div>
<form method="dialog">
<button class="btn">Okay</button>
</form>
</div>
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<dialog #terms_of_use_modal class="modal">
<div class="modal-box w-11/12 max-w-5xl">
<h3 class="font-bold text-lg">SharePulse™ Terms of Use</h3>
<div>
<div class="mx-auto my-6">
<ol class="list-decimal">
<li class="ml-4 mb-2 text-gray-800 font-semibold">Users must not upload files that contain malware, illegal content, or violate copyright laws.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">Sharepulse is not responsible for the content of the files shared through its service.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">By using Sharepulse, users agree not to attempt to breach the security of the service.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">Sharepulse reserves the right to remove any files or suspend users that violate these terms without notice.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">The service is provided "as is" without warranty of any kind, express or implied.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">Users agree to indemnify and hold harmless Sharepulse from any claims resulting from the use of the service.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">Sharepulse reserves the right to modify these terms at any time. Continued use of the service after such changes constitutes acceptance of the new terms.</li>
</ol>
</div>
<form method="dialog">
<button class="btn">Okay</button>
</form>
</div>
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<!-- Invisible SVGs to prevent lazy loading --> <!-- Invisible SVGs to prevent lazy loading -->
<div class="invisible h-0 w-0"> <div class="invisible h-0 w-0">
<img src="./assets/cloud-arrow-up-solid.svg"> <img src="./assets/cloud-arrow-up-solid.svg">

View File

@ -1,6 +1,7 @@
import {Component, ElementRef, ViewChild} from '@angular/core'; import {Component, ElementRef, ViewChild} from '@angular/core';
import {RouterLink} from "@angular/router"; import {RouterLink} from "@angular/router";
import {FormsModule} from "@angular/forms"; import {FormsModule} from "@angular/forms";
import {LegalService} from "../../service/legalService";
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
@ -13,15 +14,14 @@ import {FormsModule} from "@angular/forms";
styleUrl: './home.component.scss' styleUrl: './home.component.scss'
}) })
export class HomeComponent { export class HomeComponent {
@ViewChild('privacy_policy_modal') privacy_policy_modal: ElementRef<HTMLDialogElement> | undefined; constructor(private legalService: LegalService) {
@ViewChild('terms_of_use_modal') terms_of_use_modal: ElementRef<HTMLDialogElement> | undefined;
openPrivacyPolicy() {
this.privacy_policy_modal?.nativeElement.showModal();
} }
openTermsOfUse() { openPrivacyPolicyModal() {
this.terms_of_use_modal?.nativeElement.showModal(); this.legalService.openPrivacyPolicy();
}
openTermsOfUseModal() {
this.legalService.openTermsOfUse();
} }
} }

View File

@ -6,7 +6,7 @@
<span class="self-center text-xl font-semibold whitespace-nowrap">SharePulse</span> <span class="self-center text-xl font-semibold whitespace-nowrap">SharePulse</span>
</a> </a>
<div class="flex items-center lg:order-2"> <div class="flex items-center lg:order-2">
<a routerLink="/credits" class="text-gray-800 hover:bg-gray-50 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2">Administration</a> <a routerLink="/credits" class="text-gray-800 hover:bg-gray-50 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2"><!--Administration-->About</a>
<button (click)="toggleMenu()" type="button" class="inline-flex items-center p-2 ml-1 text-sm text-gray-500 rounded-lg lg:hidden hover:bg-gray-100 " aria-controls="mobile-menu-2" aria-expanded="false"> <button (click)="toggleMenu()" type="button" class="inline-flex items-center p-2 ml-1 text-sm text-gray-500 rounded-lg lg:hidden hover:bg-gray-100 " aria-controls="mobile-menu-2" aria-expanded="false">
<span class="sr-only">Open main menu</span> <span class="sr-only">Open main menu</span>
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path></svg> <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path></svg>
@ -26,3 +26,65 @@
</div> </div>
</nav> </nav>
</header> </header>
<dialog #privacy_policy_modal class="modal">
<div class="modal-box w-11/12 max-w-5xl">
<h3 class="font-bold text-lg">SharePulse™ Privacy Policy</h3>
<div>
<div class="mx-auto my-6">
<ol class="list-decimal">
<li class="ml-4 mb-2 text-gray-800 font-semibold">Uploaded files are stored on a secure server.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">Files are stored for a maximum of 24 hours.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">SharePulse tracks your IP address when uploading files. This serves as a method of identification, allowing you to check the status of your upload.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">SharePulse may track your IP address when downloading files.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">SharePulse does not track you when accessing the website.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">Personal information collected by SharePulse is used solely for data protection and is not processed.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">SharePulse will not sell, rent, or share personal information with third parties without user consent, except as required by law.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">Users have the right to request the deletion of their data from SharePulse's servers at any time.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">SharePulse does not store any cookies on your device.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">Operators do not view or manipulate uploaded files.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">Users are encouraged to review the privacy policy periodically for any changes or updates.</li>
</ol>
</div>
<form method="dialog">
<button class="btn">Okay</button>
</form>
</div>
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<dialog #terms_of_use_modal class="modal">
<div class="modal-box w-11/12 max-w-5xl">
<h3 class="font-bold text-lg">SharePulse™ Terms of Use</h3>
<div>
<div class="mx-auto my-6">
<ol class="list-decimal">
<li class="ml-4 mb-2 text-gray-800 font-semibold">Users must not upload files that contain malware, illegal content, or violate copyright laws.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">SharePulse is not responsible for the content of the files shared through its service.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">By using SharePulse, users agree not to attempt to breach the security of the service.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">SharePulse reserves the right to remove any files or suspend users that violate these terms without notice.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">The service is provided "as is" without warranty of any kind, express or implied.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">Users agree to indemnify and hold harmless SharePulse from any claims resulting from the use of the service.</li>
<li class="ml-4 mb-2 text-gray-800 font-semibold">SharePulse reserves the right to modify these terms at any time. Continued use of the service after such changes constitutes acceptance of the new terms.</li>
</ol>
</div>
<form method="dialog">
<button class="btn">Okay</button>
</form>
</div>
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>

View File

@ -1,21 +1,46 @@
import { Component } from '@angular/core'; import {Component, ElementRef, ViewChild} from '@angular/core';
import {RouterLink} from "@angular/router"; import {RouterLink} from "@angular/router";
import {NgClass} from "@angular/common"; import {NgClass} from "@angular/common";
import {FormsModule} from "@angular/forms";
import {LegalService} from "../../service/legalService";
@Component({ @Component({
selector: 'app-navbar', selector: 'app-navbar',
standalone: true, standalone: true,
imports: [ imports: [
RouterLink, RouterLink,
NgClass NgClass,
FormsModule
], ],
templateUrl: './navbar.component.html', templateUrl: './navbar.component.html',
styleUrl: './navbar.component.scss' styleUrl: './navbar.component.scss'
}) })
export class NavbarComponent { export class NavbarComponent {
@ViewChild('privacy_policy_modal') privacy_policy_modal: ElementRef<HTMLDialogElement> | undefined;
@ViewChild('terms_of_use_modal') terms_of_use_modal: ElementRef<HTMLDialogElement> | undefined;
isMenuOpen = false; isMenuOpen = false;
constructor(private legalService: LegalService) {
this.legalService.openModal$.subscribe((modalId) => {
if (modalId === 'privacyPolicy') {
this.openPrivacyPolicy();
} else if (modalId === 'termsOfUse') {
this.openTermsOfUse();
}
});
}
toggleMenu(): void { toggleMenu(): void {
this.isMenuOpen = !this.isMenuOpen; this.isMenuOpen = !this.isMenuOpen;
} }
openPrivacyPolicy() {
this.privacy_policy_modal?.nativeElement.showModal();
}
openTermsOfUse() {
this.terms_of_use_modal?.nativeElement.showModal();
}
} }

View File

@ -28,17 +28,18 @@
<div class="w-full mb-10"> <div class="w-full mb-10">
<label class="block text-gray-700 text-l font-bold mb-4">Options</label> <label class="block text-gray-700 text-l font-bold mb-4">Options</label>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="flex items-center"> <div class="flex items-center" >
<input id="shortStorage" [(ngModel)]="shortStorage" type="checkbox" class="toggle" checked /> <input id="shortStorage" [(ngModel)]="shortStorage" type="checkbox" class="toggle" [disabled]="true"/>
<label for="shortStorage" class="ml-2 text-gray-700">Store for only one hour</label> <label for="shortStorage" class="ml-2 text-gray-700 tooltip" data-tip="This feature will be available with the next update!">Store for only one hour</label>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<input id="singleDownload" [(ngModel)]="singleDownload" type="checkbox" class="toggle" /> <input id="singleDownload" [(ngModel)]="singleDownload" type="checkbox" class="toggle" />
<label for="singleDownload" class="ml-2 text-gray-700">Allow only one download</label> <label for="singleDownload" class="ml-2 text-gray-700 tooltip" data-tip="If enabled, the uploaded file can only be downloaded once.">Allow only one download</label>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<input id="passwordProtected" [(ngModel)]="passwordProtected" type="checkbox" class="toggle" /> <input id="passwordProtected" [(ngModel)]="passwordProtected" type="checkbox" class="toggle" />
<label for="passwordProtected" class="ml-2 text-gray-700">Protect download with password</label> <label for="passwordProtected" class="ml-2 text-gray-700 tooltip"
data-tip="The password will be generated by the server and is displayed after the upload is finished.">Protect download with password</label>
</div> </div>
</div> </div>
</div> </div>

View File

@ -115,14 +115,19 @@ export class UploadComponent {
buildFileUrls(fileDetails: FileDetails) { buildFileUrls(fileDetails: FileDetails) {
const baseUrl = this.developmentStore.getBaseUrl(); const baseUrl = this.developmentStore.getBaseUrl();
const fileId = fileDetails.fileId; const fileId = fileDetails.fileId;
const downloadUrl = `${baseUrl}download?fileId=${fileId}`; let passwordUrlPart = '';
const deleteUrl = `${baseUrl}api/v1/deletefile?fileId=${fileId}`; if(fileDetails.passwordProtected) {
const statusUrl = `${baseUrl}status?fileId=${fileId}`; passwordUrlPart = `&password=${fileDetails.password}`;
}
const downloadUrl = `${baseUrl}download?fileId=${fileId}${passwordUrlPart}`;
const deleteUrl = `${baseUrl}api/v1/deletefile?fileId=${fileId}${passwordUrlPart}`;
const statusUrl = `${baseUrl}status?fileId=${fileId}${passwordUrlPart}`;
return { return {
downloadUrl, downloadUrl,
statusUrl, statusUrl,
deleteUrl, deleteUrl,
}; } };
}
buildFormDataObject(): FormData { buildFormDataObject(): FormData {
const formData = new FormData(); const formData = new FormData();

View File

@ -0,0 +1,23 @@
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable({
providedIn: 'root',
})
/**
* Service to open the privacy policy and terms of use modals
* The modals are controlled by the navbar component
*/
export class LegalService {
private openModalSource = new Subject<string>();
openModal$ = this.openModalSource.asObservable();
openPrivacyPolicy() {
this.openModalSource.next('privacyPolicy');
}
openTermsOfUse() {
this.openModalSource.next('termsOfUse');
}
}

View File

@ -1,9 +1,11 @@
package de.w665.sharepulse.config; package de.w665.sharepulse.config;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@EnableScheduling
@Configuration @Configuration
public class MvcConfig implements WebMvcConfigurer { public class MvcConfig implements WebMvcConfigurer {

View File

@ -35,22 +35,6 @@ public class RethinkDBService {
@PostConstruct @PostConstruct
public void initialize() { public void initialize() {
//rethinkdb check if database exists
/*Result<Object> result = r.dbList().run(connection);
List<String> databases = new ArrayList<>();
Object db = result.first();
ArrayList<String> dbList = (ArrayList<String>) db;
databases.addAll(dbList);
if (!databases.contains("sharepulse")) {
r.dbCreate("sharepulse").run(connection);
log.info("Database 'sharepulse' created successfully.");
} else {
log.info("Database 'sharepulse' already exists. No action needed.");
}*/
// rethinkdb check if database exists // rethinkdb check if database exists
try { try {
r.dbCreate(config.getDatabase()).run(connection).stream(); r.dbCreate(config.getDatabase()).run(connection).stream();
@ -77,7 +61,25 @@ public class RethinkDBService {
r.db(config.getDatabase()).tableCreate("id_store").run(connection).stream(); r.db(config.getDatabase()).tableCreate("id_store").run(connection).stream();
log.debug("Table 'id_store' created successfully."); log.debug("Table 'id_store' created successfully.");
} catch (ReqlOpFailedError e) { } catch (ReqlOpFailedError e) {
log.debug("Table 'id_store' already exists. No action needed."); log.debug("Table 'id_store' already exists.");
if(autoResetOnStartup) {
log.debug("Clearing content...");
r.db(config.getDatabase()).table("id_store").delete().run(connection);
log.debug("Table 'id_store' cleared successfully.");
}
}
// rethinkdb check if table expired_file_uploads exists
try {
r.db(config.getDatabase()).tableCreate("expired_file_uploads").run(connection).stream();
log.debug("Table 'expired_file_uploads' created successfully.");
} catch (ReqlOpFailedError e) {
log.debug("Table 'expired_file_uploads' already exists.");
if(autoResetOnStartup) {
log.debug("Clearing content...");
r.db(config.getDatabase()).table("expired_file_uploads").delete().run(connection);
log.debug("Table 'expired_file_uploads' cleared successfully.");
}
} }
log.info("Database ready for operation!"); log.info("Database ready for operation!");
} }

View File

@ -0,0 +1,41 @@
package de.w665.sharepulse.db.repo;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.rethinkdb.RethinkDB;
import com.rethinkdb.net.Connection;
import de.w665.sharepulse.db.RethinkDBConnector;
import de.w665.sharepulse.model.FileUpload;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.lang.reflect.Type;
import java.util.Map;
@Repository
public class ExpiredFileUploadRepository {
private final RethinkDB r;
private final Connection connection;
private final Gson gson;
@Autowired
public ExpiredFileUploadRepository(RethinkDBConnector connector) {
this.r = connector.getR();
this.connection = connector.getConnection();
this.gson = new GsonBuilder().create();
}
public void insertExpiredFileUpload(FileUpload fileUpload) {
String json = gson.toJson(fileUpload);
Type type = new TypeToken<Map<String, Object>>(){}.getType();
Map<String, Object> map = gson.fromJson(json, type);
long uploadDateTimestamp = fileUpload.getUploadDate().getTime() / 1000;
map.put("uploadDate", uploadDateTimestamp);
r.db("sharepulse").table("expired_file_uploads").insert(map).run(connection);
}
}

View File

@ -0,0 +1,37 @@
package de.w665.sharepulse.db.repo;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.rethinkdb.RethinkDB;
import com.rethinkdb.net.Connection;
import com.rethinkdb.net.Result;
import de.w665.sharepulse.db.RethinkDBConnector;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class FileIdRepository {
private final RethinkDB r;
private final Connection connection;
private final Gson gson;
@Autowired
public FileIdRepository(RethinkDBConnector connector) {
this.r = connector.getR();
this.connection = connector.getConnection();
this.gson = new GsonBuilder().create();
}
public void insertFileId(String fileId) {
r.db("sharepulse").table("id_store").insert(r.hashMap("fileId", fileId)).run(connection);
}
public boolean fileIdExists(String fileId) {
Result<Object> result = r.db("sharepulse").table("id_store")
.filter(r.hashMap("fileId", fileId))
.run(connection);
return result.hasNext();
}
}

View File

@ -6,35 +6,28 @@ import com.google.gson.reflect.TypeToken;
import com.rethinkdb.RethinkDB; import com.rethinkdb.RethinkDB;
import com.rethinkdb.net.Connection; import com.rethinkdb.net.Connection;
import de.w665.sharepulse.db.RethinkDBConnector; import de.w665.sharepulse.db.RethinkDBConnector;
import de.w665.sharepulse.db.RethinkDBService;
import de.w665.sharepulse.model.FileUpload; import de.w665.sharepulse.model.FileUpload;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.Map; import java.util.*;
import java.util.NoSuchElementException;
import java.util.Optional;
@Repository @Repository
@RequiredArgsConstructor @RequiredArgsConstructor
public class FileUploadRepository { public class FileUploadRepository {
private final RethinkDBService rethinkDBService;
private final RethinkDB r; private final RethinkDB r;
private final Connection connection; private final Connection connection;
private final Gson gson; private final Gson gson;
@Autowired @Autowired
public FileUploadRepository(RethinkDBService rethinkDBService, RethinkDBConnector connector) { public FileUploadRepository(RethinkDBConnector connector) {
this.rethinkDBService = rethinkDBService;
this.r = connector.getR(); this.r = connector.getR();
this.connection = connector.getConnection(); this.connection = connector.getConnection();
this.gson = new GsonBuilder() this.gson = new GsonBuilder().create();
.setDateFormat("dd-MM-yyyy HH:mm:ss") // date field formatting
.create();
} }
public void insertFileUpload(FileUpload fileUpload) { public void insertFileUpload(FileUpload fileUpload) {
@ -43,6 +36,9 @@ public class FileUploadRepository {
Type type = new TypeToken<Map<String, Object>>(){}.getType(); Type type = new TypeToken<Map<String, Object>>(){}.getType();
Map<String, Object> map = gson.fromJson(json, type); Map<String, Object> map = gson.fromJson(json, type);
long uploadDateTimestamp = fileUpload.getUploadDate().getTime() / 1000;
map.put("uploadDate", uploadDateTimestamp);
r.db("sharepulse").table("file_uploads").insert(map).run(connection); r.db("sharepulse").table("file_uploads").insert(map).run(connection);
} }
@ -72,4 +68,34 @@ public class FileUploadRepository {
.update(map) .update(map)
.run(connection); .run(connection);
} }
public void deleteFileUploadByFileId(String fileId) {
r.db("sharepulse").table("file_uploads")
.filter(r.hashMap("fileId", fileId))
.delete()
.run(connection);
}
public List<FileUpload> getAllExpiredFileUploads() {
long timestamp = getOneMinuteAgoTimestamp();
List<FileUpload> olderFiles = r.db("sharepulse").table("file_uploads")
.filter(row -> row.g("uploadDate").lt(timestamp))
.run(connection, FileUpload.class)
.toList();
return olderFiles;
}
private long get24HoursAgoTimestamp() {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.HOUR, -24);
return calendar.getTimeInMillis() / 1000;
}
private long getOneMinuteAgoTimestamp() {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE, -1); // Subtract 1 minute
return calendar.getTimeInMillis() / 1000; // Convert milliseconds to seconds (Unix timestamp)
}
} }

View File

@ -18,7 +18,7 @@ public class FileUpload {
private String fileName; private String fileName;
private long fileSize; private long fileSize;
private boolean singleDownload; private boolean singleDownload;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy HH:mm:ss", timezone = "CET") @JsonFormat(timezone = "ETC")
private Date uploadDate; private Date uploadDate;
private String uploadedByIpAddress; private String uploadedByIpAddress;
private long downloadCount; private long downloadCount;

View File

@ -82,7 +82,7 @@ public class Download extends ApiRestController {
Optional<FileUpload> optionalFileUpload = fileService.getFileUploadByFileId(fileId); Optional<FileUpload> optionalFileUpload = fileService.getFileUploadByFileId(fileId);
FileUpload fileUpload = optionalFileUpload.orElse(null); FileUpload fileUpload = optionalFileUpload.orElse(null);
if(optionalFileUpload.isEmpty()) { if(optionalFileUpload.isEmpty()) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND); return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
} }
boolean downloadable = !fileUpload.isSingleDownload() || fileUpload.getDownloadCount() == 0; boolean downloadable = !fileUpload.isSingleDownload() || fileUpload.getDownloadCount() == 0;

View File

@ -0,0 +1,36 @@
package de.w665.sharepulse.service;
import de.w665.sharepulse.db.repo.FileUploadRepository;
import de.w665.sharepulse.db.repo.ExpiredFileUploadRepository;
import de.w665.sharepulse.model.FileUpload;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.List;
@Slf4j
@Service
public class FileCleanupService {
private final FileUploadRepository fileUploadRepository;
private final ExpiredFileUploadRepository expiredFileUploadRepository;
private final FileService fileService;
public FileCleanupService(FileUploadRepository fileUploadRepository, ExpiredFileUploadRepository expiredFileUploadRepository, FileService fileService) {
this.fileUploadRepository = fileUploadRepository;
this.expiredFileUploadRepository = expiredFileUploadRepository;
this.fileService = fileService;
}
@Scheduled(cron = "0 0 * * * *")
public void cleanup() {
log.debug("Running cleanup...");
List<FileUpload> expFileUploads = fileUploadRepository.getAllExpiredFileUploads();
for (FileUpload fileUpload : expFileUploads) {
fileService.deleteFile(fileUpload);
expiredFileUploadRepository.insertExpiredFileUpload(fileUpload);
log.debug("Moved file " + fileUpload.getFileId() + " to old_file_uploads table.");
}
}
}

View File

@ -1,34 +1,51 @@
package de.w665.sharepulse.service; package de.w665.sharepulse.service;
import de.w665.sharepulse.db.repo.FileIdRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.security.SecureRandom;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
@Service @Service
public class FileIdService { public class FileIdService {
private static int dailyCounter = 0; @Value("${sharepulse.fileid.length}")
private static String lastDate = ""; private int fileIdLength;
// Not safe to use @Value("${sharepulse.fileid.charset}")
public static synchronized String generateId() { private String CHARSET;
String today = new SimpleDateFormat("yyMMdd").format(new Date());
if (!today.equals(lastDate)) { private final FileIdRepository fileIdRepository;
dailyCounter = 0; private static final SecureRandom RANDOM = new SecureRandom();
lastDate = today;
@Autowired
public FileIdService(FileIdRepository fileIdRepository) {
this.fileIdRepository = fileIdRepository;
} }
String counterEncoded = Integer.toString(++dailyCounter, 36).toUpperCase(); private String generateId() {
String uniqueId;
do {
uniqueId = generateRandomCode(fileIdLength);
} while (fileIdRepository.fileIdExists(uniqueId));
fileIdRepository.insertFileId(uniqueId);
return uniqueId;
}
return today + counterEncoded; private String generateRandomCode(int length) {
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
int index = RANDOM.nextInt(CHARSET.length());
sb.append(CHARSET.charAt(index));
}
return sb.toString();
} }
public String generateNewUniqueId() { public String generateNewUniqueId() {
return generateId(); return generateId();
} }
} }

View File

@ -5,17 +5,19 @@ import de.w665.sharepulse.model.FileUpload;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.security.SecureRandom;
import java.util.Random; import java.util.Random;
@Slf4j @Slf4j
@Service @Service
public class FileSecurityService { public class FileSecurityService {
@Value("${sharepulse.filepassword-length}") @Value("${sharepulse.filepassword.length}")
private int passwordLength; private int passwordLength;
@Value("${sharepulse.filepassword-charset}") @Value("${sharepulse.filepassword.charset}")
private String passwordCharset; private String passwordCharset;
private final Random random = new Random(); private final Random random = new SecureRandom();
public boolean verifyDownloadPermission(FileUpload file, String password) throws NoDownloadPermissionException { public boolean verifyDownloadPermission(FileUpload file, String password) throws NoDownloadPermissionException {

View File

@ -96,7 +96,6 @@ public class FileService {
fileUploadRepository.insertFileUpload(fileUpload); fileUploadRepository.insertFileUpload(fileUpload);
// TODO: rename file to fileID
Path path = Paths.get(getTempDirPath() + File.separator + fileId); Path path = Paths.get(getTempDirPath() + File.separator + fileId);
try { try {
Files.write(path, file.getBytes()); Files.write(path, file.getBytes());
@ -122,6 +121,16 @@ public class FileService {
return fileUpload.getDownloadCount(); return fileUpload.getDownloadCount();
} }
public void deleteFile(FileUpload fileUpload) {
fileUploadRepository.deleteFileUploadByFileId(fileUpload.getFileId());
Path path = Paths.get(getTempDirPath() + File.separator + fileUpload.getFileId());
try {
Files.delete(path);
log.debug("File " + fileUpload.getFileId() + " deleted from disk.");
} catch (IOException e) {
log.error("Error deleting file " + fileUpload.getFileId() + " from disk: " + e.getMessage());
}
}
private String getTempDirPath() { private String getTempDirPath() {
return System.getProperty("user.dir") + tempDirPath; return System.getProperty("user.dir") + tempDirPath;

View File

@ -1,8 +1,10 @@
# Application config # Application config
sharepulse.temp-filestore-path=/temp-filestore sharepulse.temp-filestore-path=/temp-filestore
sharepulse.filepassword-length=6 sharepulse.auto-reset-on-startup=true
sharepulse.filepassword-charset=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 sharepulse.fileid.length=6
sharepulse.auto-reset-on-startup=false sharepulse.fileid.charset=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
sharepulse.filepassword.length=6
sharepulse.filepassword.charset=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
# Static path # Static path
spring.web.resources.static-locations=classpath:/static/browser/ spring.web.resources.static-locations=classpath:/static/browser/