diff --git a/README.md b/README.md
index 1896f80..b3f689c 100644
--- a/README.md
+++ b/README.md
@@ -57,4 +57,7 @@ networks:
volumes:
rethinkdb_data:
-```
\ No newline at end of file
+```
+
+### Dev notes
+Fix slow build times: Use Gradle Build and Run settings set to IntelliJ IDEA
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 45eb111..a8d6cb7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -5,7 +5,7 @@ plugins {
}
group = 'de.w665'
-version = '1.1.1'
+version = '1.1.2'
java {
sourceCompatibility = '21'
@@ -36,6 +36,15 @@ dependencies {
implementation 'com.rethinkdb:rethinkdb-driver:2.4.4'
// https://mvnrepository.com/artifact/com.google.code.gson/gson
implementation group: 'com.google.code.gson', name: 'gson', version: '2.10.1'
+ // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security
+ implementation group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '3.2.4'
+ // https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api
+ implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.12.5'
+ // https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl
+ runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.12.5'
+ // https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-orgjson
+ runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-orgjson', version: '0.12.5'
+
}
bootJar {
diff --git a/frontend/angular.json b/frontend/angular.json
index 8fbc1d7..3c4ab0f 100644
--- a/frontend/angular.json
+++ b/frontend/angular.json
@@ -97,5 +97,8 @@
}
}
}
+ },
+ "cli": {
+ "analytics": false
}
}
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 4a078b5..27cc285 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -5945,17 +5945,17 @@
"dev": true
},
"node_modules/express": {
- "version": "4.18.2",
- "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
- "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
+ "version": "4.19.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
+ "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"dev": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
- "body-parser": "1.20.1",
+ "body-parser": "1.20.2",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
- "cookie": "0.5.0",
+ "cookie": "0.6.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@@ -5986,34 +5986,10 @@
"node": ">= 0.10.0"
}
},
- "node_modules/express/node_modules/body-parser": {
- "version": "1.20.1",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
- "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
- "dev": true,
- "dependencies": {
- "bytes": "3.1.2",
- "content-type": "~1.0.4",
- "debug": "2.6.9",
- "depd": "2.0.0",
- "destroy": "1.2.0",
- "http-errors": "2.0.0",
- "iconv-lite": "0.4.24",
- "on-finished": "2.4.1",
- "qs": "6.11.0",
- "raw-body": "2.5.1",
- "type-is": "~1.6.18",
- "unpipe": "1.0.0"
- },
- "engines": {
- "node": ">= 0.8",
- "npm": "1.2.8000 || >= 1.4.16"
- }
- },
"node_modules/express/node_modules/cookie": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
- "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"dev": true,
"engines": {
"node": ">= 0.6"
@@ -6052,21 +6028,6 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true
},
- "node_modules/express/node_modules/raw-body": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
- "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
- "dev": true,
- "dependencies": {
- "bytes": "3.1.2",
- "http-errors": "2.0.0",
- "iconv-lite": "0.4.24",
- "unpipe": "1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/express/node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@@ -6278,9 +6239,9 @@
"dev": true
},
"node_modules/follow-redirects": {
- "version": "1.15.5",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
- "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
+ "version": "1.15.6",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+ "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [
{
"type": "individual",
@@ -7076,9 +7037,9 @@
}
},
"node_modules/ip": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
- "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz",
+ "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==",
"dev": true
},
"node_modules/ipaddr.js": {
@@ -12007,9 +11968,9 @@
}
},
"node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz",
- "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==",
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz",
+ "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==",
"dev": true,
"dependencies": {
"colorette": "^2.0.10",
diff --git a/frontend/src/app/adminui/adminui.component.html b/frontend/src/app/adminui/adminui.component.html
new file mode 100644
index 0000000..27a5447
--- /dev/null
+++ b/frontend/src/app/adminui/adminui.component.html
@@ -0,0 +1,173 @@
+
+
Admin Dashboard
+
+
+
+
+
+
Total Files Uploaded
+
{{ fileUploads.length + expiredFileUploads.length }}
+
Since launch
+
+
+
+
+
+
Total File Size on Disk
+
{{ totalFileSizeOnDisk | formatFileSizePipe }}
+
Across all stored files
+
+
+
+
+
+
Operational For
+
{{ statistics.applicationOnlineTime | duration }}
+
Since launch
+
+
+
+
+
+
Total Downloads
+
{{ totalFileDownloads }}
+
All time
+
+
+
+
+
+
Last Admin Login
+
{{ statistics.lastUserLogin.loginTime | relativeTime }}
+
First login...
+
Most recent login
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Active file uploads
+
+
+
+
+ Entity ID |
+ File ID |
+ File Name |
+ File Size |
+ Single Download |
+ Disabled |
+ Upload Date |
+ Uploaded By IP |
+ Download Count |
+
+ Password Protected |
+
+
+
+
+ {{ file.id }} |
+ {{ file.fileId }} |
+ {{ file.fileName }} |
+ {{ file.fileSize | formatFileSizePipe }} |
+ {{ file.singleDownload ? 'true' : 'false' }} |
+
+
+ {{ file.disabled ? 'true' : 'false' }}
+
+
+ |
+ {{ file.uploadDate | date: 'medium' }} |
+ {{ file.uploadedByIpAddress }} |
+ {{ file.downloadCount }} |
+
+ {{ file.passwordProtected ? 'true' : 'false' }} |
+
+
+
+
+
+
Expired file uploads
+
+
+
+
+ Entity ID |
+ File ID |
+ File Name |
+ File Size |
+ Single Download |
+ Disabled |
+ Upload Date |
+ Uploaded By IP |
+ Download Count |
+
+ Password Protected |
+
+
+
+
+ {{ file.id }} |
+ {{ file.fileId }} |
+ {{ file.fileName }} |
+ {{ file.fileSize | formatFileSizePipe }} |
+ {{ file.singleDownload ? 'true' : 'false' }} |
+ {{ file.disabled ? 'true' : 'false' }} |
+ {{ file.uploadDate | date: 'medium' }} |
+ {{ file.uploadedByIpAddress }} |
+ {{ file.downloadCount }} |
+
+ {{ file.passwordProtected ? 'true' : 'false' }} |
+
+
+
+
+
+
+
+
diff --git a/frontend/src/app/adminui/adminui.component.scss b/frontend/src/app/adminui/adminui.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/app/adminui/adminui.component.spec.ts b/frontend/src/app/adminui/adminui.component.spec.ts
new file mode 100644
index 0000000..68793fb
--- /dev/null
+++ b/frontend/src/app/adminui/adminui.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AdminuiComponent } from './adminui.component';
+
+describe('AdminuiComponent', () => {
+ let component: AdminuiComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [AdminuiComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(AdminuiComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/adminui/adminui.component.ts b/frontend/src/app/adminui/adminui.component.ts
new file mode 100644
index 0000000..1bc569c
--- /dev/null
+++ b/frontend/src/app/adminui/adminui.component.ts
@@ -0,0 +1,181 @@
+import {Component, ElementRef, ViewChild} from '@angular/core';
+import {DatePipe, DecimalPipe, NgForOf, NgIf} from "@angular/common";
+import axios from "axios";
+import {firstValueFrom} from "rxjs";
+import {DevelopmentStore} from "../../store/DevelopmentStore";
+import {AuthStore} from "../../store/authStore";
+import {Router} from "@angular/router";
+import {FormatFileSizePipePipe} from "../format-file-size-pipe.pipe";
+import {DurationPipe} from "../duration.pipe";
+import {RelativeTimePipe} from "../relative-time.pipe";
+import {FormsModule} from "@angular/forms";
+import {EdituserComponent} from "./edituser/edituser.component";
+import {LoginhistoryComponent} from "./loginhistory/loginhistory.component";
+
+@Component({
+ selector: 'app-adminui',
+ standalone: true,
+ imports: [
+ DatePipe,
+ DecimalPipe,
+ NgForOf,
+ FormatFileSizePipePipe,
+ DurationPipe,
+ RelativeTimePipe,
+ FormsModule,
+ EdituserComponent,
+ NgIf,
+ LoginhistoryComponent
+ ],
+ templateUrl: './adminui.component.html',
+ styleUrl: './adminui.component.scss'
+})
+export class AdminuiComponent {
+
+ @ViewChild('edit_user_modal') edit_user_modal: ElementRef | undefined;
+ @ViewChild('login_history_modal') login_history_modal: ElementRef | undefined;
+
+ fileUploads: any[] = [];
+ expiredFileUploads: any[] = [];
+ totalFileSizeOnDisk: number = 0;
+ totalFileDownloads = 0;
+ statistics: any = "";
+ username: string = "";
+
+ constructor(private developmentStore: DevelopmentStore, private authStore: AuthStore, private router: Router) {
+ this.init();
+ }
+
+ async init() {
+ this.username = await firstValueFrom(this.authStore.username$);
+ await this.verifyToken();
+ setInterval(() => {
+ this.verifyToken();
+ }, 5000);
+ }
+
+ async verifyToken() {
+ if(await firstValueFrom(this.authStore.token$) === "") {
+ console.log("No token present, redirecting to login...");
+ await this.router.navigate(['/login']);
+ return;
+ }
+ await this.fetchFileUploads();
+ await this.fetchExpiredFileUploads();
+ await this.fetchStatistics();
+ await this.calculateStatistics();
+ }
+
+ async calculateStatistics() {
+ this.totalFileSizeOnDisk = 0;
+ this.totalFileDownloads = 0;
+
+ for(let fileUpload of this.fileUploads) {
+ this.totalFileSizeOnDisk += fileUpload.fileSize;
+ }
+
+ for(let fileUpload of this.expiredFileUploads) {
+ this.totalFileDownloads += fileUpload.downloadCount;
+ }
+ for(let fileUpload of this.fileUploads) {
+ this.totalFileDownloads += fileUpload.downloadCount;
+ }
+ }
+
+ openEditUserModal() {
+ this.edit_user_modal?.nativeElement.showModal();
+ }
+
+ openLoginHistoryModal() {
+ this.login_history_modal?.nativeElement.showModal();
+ }
+
+ logout() {
+ this.authStore.setToken("");
+ this.authStore.setUsername("");
+ this.router.navigate(['/login']);
+ }
+
+ async fetchFileUploads() {
+ try {
+ const response = await axios({
+ method: 'get',
+ url: this.developmentStore.getBaseUrl() + 'api/v1/secure/upload-history',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + await firstValueFrom(this.authStore.token$)
+ }
+ });
+ this.fileUploads = response.data;
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ async fetchExpiredFileUploads() {
+ try {
+ const response = await axios({
+ method: 'get',
+ url: this.developmentStore.getBaseUrl() + 'api/v1/secure/expired-upload-history',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + await firstValueFrom(this.authStore.token$)
+ }
+ });
+ this.expiredFileUploads = response.data;
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ async fetchStatistics() {
+ try {
+ const response = await axios({
+ method: 'get',
+ url: this.developmentStore.getBaseUrl() + 'api/v1/secure/statistics',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + await firstValueFrom(this.authStore.token$)
+ }
+ });
+ this.statistics = response.data;
+ //console.log(this.statistics)
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ async deleteAllFileUploads() {
+ try {
+ const response = await axios({
+ method: 'delete',
+ url: this.developmentStore.getBaseUrl() + 'api/v1/secure/files',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + await firstValueFrom(this.authStore.token$)
+ }
+ });
+ console.log(response.data)
+ } catch (error) {
+ console.error(error);
+ }
+ }
+
+ async disableFile(fileId: string) {
+ try {
+ const response = await axios({
+ method: 'put',
+ url: this.developmentStore.getBaseUrl() + 'api/v1/secure/files/disable',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + await firstValueFrom(this.authStore.token$)
+ },
+ data: {
+ fileId: fileId
+ }
+ });
+ console.log(response.data)
+ } catch (error) {
+ console.error(error);
+ }
+ }
+
+ protected readonly confirm = confirm;
+}
diff --git a/frontend/src/app/adminui/edituser/edituser.component.html b/frontend/src/app/adminui/edituser/edituser.component.html
new file mode 100644
index 0000000..2dd483d
--- /dev/null
+++ b/frontend/src/app/adminui/edituser/edituser.component.html
@@ -0,0 +1,45 @@
+
+
diff --git a/frontend/src/app/adminui/edituser/edituser.component.scss b/frontend/src/app/adminui/edituser/edituser.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/app/adminui/edituser/edituser.component.spec.ts b/frontend/src/app/adminui/edituser/edituser.component.spec.ts
new file mode 100644
index 0000000..3537722
--- /dev/null
+++ b/frontend/src/app/adminui/edituser/edituser.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { EdituserComponent } from './edituser.component';
+
+describe('EdituserComponent', () => {
+ let component: EdituserComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [EdituserComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(EdituserComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/adminui/edituser/edituser.component.ts b/frontend/src/app/adminui/edituser/edituser.component.ts
new file mode 100644
index 0000000..ba96fb7
--- /dev/null
+++ b/frontend/src/app/adminui/edituser/edituser.component.ts
@@ -0,0 +1,63 @@
+import {Component, Input, SimpleChanges} from '@angular/core';
+import {FormsModule} from "@angular/forms";
+import axios from "axios";
+import {firstValueFrom} from "rxjs";
+import {DevelopmentStore} from "../../../store/DevelopmentStore";
+import {AuthStore} from "../../../store/authStore";
+import {Router} from "@angular/router";
+
+@Component({
+ selector: 'app-edituser',
+ standalone: true,
+ imports: [
+ FormsModule
+ ],
+ templateUrl: './edituser.component.html',
+ styleUrl: './edituser.component.scss'
+})
+export class EdituserComponent {
+ @Input("username") parsedUsername: string = "";
+ username: string = "";
+ originalPassword: string = "";
+ newPassword: string = "";
+ confirmNewPassword: string = "";
+
+ constructor(private developmentStore: DevelopmentStore, private authStore: AuthStore, private router: Router) {}
+
+ async saveUser() {
+
+ if(this.newPassword !== this.confirmNewPassword) {
+ alert("New password and confirm password do not match");
+ return;
+ }
+
+ try {
+ const response = await axios({
+ method: 'post',
+ url: this.developmentStore.getBaseUrl() + 'api/v1/secure/users',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + await firstValueFrom(this.authStore.token$)
+ },
+ data: {
+ originalUsername: this.parsedUsername,
+ username: this.username,
+ originalPassword: this.originalPassword,
+ newPassword: this.newPassword,
+ newPasswordConfirm: this.confirmNewPassword
+ }
+ });
+ console.log("User updated successfully");
+ alert("User updated successfully. Please log in again to continue.");
+ await this.router.navigate(['/login']);
+ } catch (error) {
+ console.error(error);
+ }
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes['parsedUsername'] && !this.username) {
+ this.username = changes['parsedUsername'].currentValue;
+ }
+ }
+}
diff --git a/frontend/src/app/adminui/loginhistory/loginhistory.component.html b/frontend/src/app/adminui/loginhistory/loginhistory.component.html
new file mode 100644
index 0000000..9b0b263
--- /dev/null
+++ b/frontend/src/app/adminui/loginhistory/loginhistory.component.html
@@ -0,0 +1,26 @@
+
+Operations
+
+Login history
+
+
+
+
+
+
+ Login Time |
+ Login IP |
+
+
+
+
+
+
+ {{ entry.loginTime | date: 'dd. MMMM yyyy, HH:mm:ss' }} |
+ {{ entry.loginIp }} |
+
+
+
+
diff --git a/frontend/src/app/adminui/loginhistory/loginhistory.component.scss b/frontend/src/app/adminui/loginhistory/loginhistory.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/app/adminui/loginhistory/loginhistory.component.spec.ts b/frontend/src/app/adminui/loginhistory/loginhistory.component.spec.ts
new file mode 100644
index 0000000..4bd4b13
--- /dev/null
+++ b/frontend/src/app/adminui/loginhistory/loginhistory.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { LoginhistoryComponent } from './loginhistory.component';
+
+describe('LoginhistoryComponent', () => {
+ let component: LoginhistoryComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [LoginhistoryComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(LoginhistoryComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/adminui/loginhistory/loginhistory.component.ts b/frontend/src/app/adminui/loginhistory/loginhistory.component.ts
new file mode 100644
index 0000000..7bf22ba
--- /dev/null
+++ b/frontend/src/app/adminui/loginhistory/loginhistory.component.ts
@@ -0,0 +1,59 @@
+import {Component, Input} from '@angular/core';
+import {DevelopmentStore} from "../../../store/DevelopmentStore";
+import {AuthStore} from "../../../store/authStore";
+import axios from "axios";
+import {firstValueFrom} from "rxjs";
+import {DatePipe, NgForOf} from "@angular/common";
+
+@Component({
+ selector: 'app-loginhistory',
+ standalone: true,
+ imports: [
+ NgForOf,
+ DatePipe
+ ],
+ templateUrl: './loginhistory.component.html',
+ styleUrl: './loginhistory.component.scss'
+})
+export class LoginhistoryComponent {
+ @Input() username: string = "";
+ loginHistory: any[] = [];
+
+ constructor(private developmentStore: DevelopmentStore, private authStore: AuthStore) {
+ this.fetchUserLoginHistory();
+ }
+
+ async fetchUserLoginHistory() {
+ try {
+ const response = await axios({
+ method: 'get',
+ url: this.developmentStore.getBaseUrl() + 'api/v1/secure/loginhistory',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + await firstValueFrom(this.authStore.token$)
+ }
+ });
+ this.loginHistory = response.data;
+ console.log(this.loginHistory)
+ } catch (error) {
+ console.error(error);
+ }
+ }
+
+ async deleteLogins() {
+ try {
+ const response = await axios({
+ method: 'delete',
+ url: this.developmentStore.getBaseUrl() + 'api/v1/secure/loginhistory',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + await firstValueFrom(this.authStore.token$)
+ }
+ });
+ console.log(response.data);
+ this.fetchUserLoginHistory();
+ } catch (error) {
+ console.error(error);
+ }
+ }
+}
diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts
index 4de0e46..4f6ea4f 100644
--- a/frontend/src/app/app.routes.ts
+++ b/frontend/src/app/app.routes.ts
@@ -4,6 +4,8 @@ import {UploadComponent} from "./upload/upload.component";
import {DownloadComponent} from "./download/download.component";
import {CreditsComponent} from "./credits/credits.component";
import {LicensesComponent} from "./credits/licenses/licenses.component";
+import {LoginComponent} from "./login/login.component";
+import {AdminuiComponent} from "./adminui/adminui.component";
export const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
@@ -12,6 +14,8 @@ export const routes: Routes = [
{ path: 'download', component: DownloadComponent },
{ path: 'credits', component: CreditsComponent },
{ path: 'licenses', component: LicensesComponent },
+ { path: 'login', component: LoginComponent },
+ { path: 'secure/administration', component: AdminuiComponent},
// { path: 'download/:id', component: DownloadComponent }
{ path: '**', redirectTo: 'home' }
];
diff --git a/frontend/src/app/credits/credits.component.html b/frontend/src/app/credits/credits.component.html
index c29454e..ca61549 100644
--- a/frontend/src/app/credits/credits.component.html
+++ b/frontend/src/app/credits/credits.component.html
@@ -42,9 +42,15 @@
+
+
Privacy Policy |
Terms of Use
+
+
+
© 2024 SharePulse. All rights reserved.
+
diff --git a/frontend/src/app/credits/credits.component.ts b/frontend/src/app/credits/credits.component.ts
index 2aacf5b..35df6f2 100644
--- a/frontend/src/app/credits/credits.component.ts
+++ b/frontend/src/app/credits/credits.component.ts
@@ -32,7 +32,7 @@ export class CreditsComponent {
}
getVersion() {
- axios.get(this.developmentStore.getBaseUrl() + 'api/v1/version')
+ axios.get(this.developmentStore.getBaseUrl() + 'api/v1/public/version')
.then((response) => {
this.version = response.data;
})
diff --git a/frontend/src/app/download/download.component.html b/frontend/src/app/download/download.component.html
index 0965040..91ea066 100644
--- a/frontend/src/app/download/download.component.html
+++ b/frontend/src/app/download/download.component.html
@@ -8,7 +8,7 @@
+ (keydown.enter)="requestDownload()"/>
{
@@ -119,7 +119,7 @@ export class DownloadComponent {
private getDownloadInfo() {
axios({
method: 'get',
- url: this.developmentStore.getBaseUrl() + 'api/v1/download-info?fileId=' + this.fileId,
+ url: this.developmentStore.getBaseUrl() + 'api/v1/public/download-info?fileId=' + this.fileId,
responseType: 'json',
})
.then(response => {
@@ -138,7 +138,7 @@ export class DownloadComponent {
this.fileDownloadStarted = true;
axios({
method: 'get',
- url: this.developmentStore.getBaseUrl() + 'api/v1/download?fileId=' + this.fileId + '&password=' + this.filePassword,
+ url: this.developmentStore.getBaseUrl() + 'api/v1/public/download?fileId=' + this.fileId + '&password=' + this.filePassword,
responseType: 'arraybuffer',
onDownloadProgress: (progressEvent) => {
const endTime = new Date().getTime();
diff --git a/frontend/src/app/duration.pipe.spec.ts b/frontend/src/app/duration.pipe.spec.ts
new file mode 100644
index 0000000..e38345d
--- /dev/null
+++ b/frontend/src/app/duration.pipe.spec.ts
@@ -0,0 +1,8 @@
+import { DurationPipe } from './duration.pipe';
+
+describe('DurationPipe', () => {
+ it('create an instance', () => {
+ const pipe = new DurationPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/duration.pipe.ts b/frontend/src/app/duration.pipe.ts
new file mode 100644
index 0000000..f94ee9a
--- /dev/null
+++ b/frontend/src/app/duration.pipe.ts
@@ -0,0 +1,29 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'duration',
+ standalone: true
+})
+export class DurationPipe implements PipeTransform {
+
+ transform(value: number): string {
+ if (!value) {
+ return '0m';
+ }
+
+ let milliseconds = value;
+ const days = Math.floor(milliseconds / (24 * 60 * 60 * 1000));
+ milliseconds %= 24 * 60 * 60 * 1000;
+ const hours = Math.floor(milliseconds / (60 * 60 * 1000));
+ milliseconds %= 60 * 60 * 1000;
+ const minutes = Math.floor(milliseconds / (60 * 1000));
+
+ if (days > 0) {
+ return `${days}d ${hours}h ${minutes}m`;
+ } else if (hours > 0) {
+ return `${hours}h ${minutes}m`;
+ } else {
+ return `${minutes}m`;
+ }
+ }
+}
diff --git a/frontend/src/app/login/login.component.html b/frontend/src/app/login/login.component.html
new file mode 100644
index 0000000..2c5ff3f
--- /dev/null
+++ b/frontend/src/app/login/login.component.html
@@ -0,0 +1,46 @@
+
+
+
Login to SharePulse
+
+
+ Login to SharePulse to access the administrative dashboard and manage uploaded files. Please note that registration is disabled.
+
+
+
+
diff --git a/frontend/src/app/login/login.component.scss b/frontend/src/app/login/login.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/app/login/login.component.spec.ts b/frontend/src/app/login/login.component.spec.ts
new file mode 100644
index 0000000..1e19e5d
--- /dev/null
+++ b/frontend/src/app/login/login.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { LoginComponent } from './login.component';
+
+describe('LoginComponent', () => {
+ let component: LoginComponent;
+ let fixture: ComponentFixture
;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [LoginComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(LoginComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/login/login.component.ts b/frontend/src/app/login/login.component.ts
new file mode 100644
index 0000000..1c1ead5
--- /dev/null
+++ b/frontend/src/app/login/login.component.ts
@@ -0,0 +1,62 @@
+import { Component } from '@angular/core';
+import {DevelopmentStore} from "../../store/DevelopmentStore";
+import {FormsModule} from "@angular/forms";
+import axios from "axios";
+import {NgClass, NgIf} from "@angular/common";
+import {AuthStore} from "../../store/authStore";
+import {firstValueFrom} from "rxjs";
+import {Router} from "@angular/router";
+
+@Component({
+ selector: 'app-login',
+ standalone: true,
+ imports: [
+ FormsModule,
+ NgClass,
+ NgIf
+ ],
+ templateUrl: './login.component.html',
+ styleUrl: './login.component.scss'
+})
+export class LoginComponent {
+ inputUsername: string = "";
+ inputPassword: string = "";
+ loginFailed: boolean = false;
+ loginSuccessful: boolean = false;
+
+ constructor(private developmentStore: DevelopmentStore, private authStore: AuthStore, private router: Router) {
+ }
+
+ tryToLogin() {
+ console.log("Trying to login with username: " + this.inputUsername + " and password: " + this.inputPassword);
+
+ axios({
+ method: 'post',
+ url: this.developmentStore.getBaseUrl() + 'api/v1/auth/login',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ data: {
+ username: this.inputUsername,
+ password: this.inputPassword
+ }
+ })
+ .then(async response => {
+ console.log(response);
+ console.log(response.data);
+ if(response.data.token) {
+ this.loginSuccessful = true;
+ this.authStore.setToken(response.data.token);
+ this.authStore.setUsername(this.inputUsername);
+
+ //timeout
+ setTimeout(() => {
+ this.router.navigate(['/secure/administration']);
+ }, 500);
+ }
+ })
+ .catch(error => {
+ this.loginFailed = true;
+ });
+ }
+}
diff --git a/frontend/src/app/relative-time.pipe.spec.ts b/frontend/src/app/relative-time.pipe.spec.ts
new file mode 100644
index 0000000..ba62c09
--- /dev/null
+++ b/frontend/src/app/relative-time.pipe.spec.ts
@@ -0,0 +1,8 @@
+import { RelativeTimePipe } from './relative-time.pipe';
+
+describe('RelativeTimePipe', () => {
+ it('create an instance', () => {
+ const pipe = new RelativeTimePipe();
+ expect(pipe).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/relative-time.pipe.ts b/frontend/src/app/relative-time.pipe.ts
new file mode 100644
index 0000000..9016478
--- /dev/null
+++ b/frontend/src/app/relative-time.pipe.ts
@@ -0,0 +1,45 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'relativeTime',
+ standalone: true,
+ pure: false
+})
+export class RelativeTimePipe implements PipeTransform {
+
+ transform(value: string): string {
+ if (!value) {
+ return '';
+ }
+
+ const now = new Date();
+ const past = new Date(value);
+ const elapsed = now.getTime() - past.getTime();
+
+ const msPerSecond = 1000;
+ const msPerMinute = msPerSecond * 60;
+ const msPerHour = msPerMinute * 60;
+ const msPerDay = msPerHour * 24;
+ const msPerWeek = msPerDay * 7;
+ const msPerMonth = msPerDay * 30;
+ const msPerYear = msPerDay * 365;
+
+ if (elapsed < msPerSecond) {
+ return 'just now';
+ } else if (elapsed < msPerMinute) {
+ return `${Math.round(elapsed / msPerSecond)} seconds ago`;
+ } else if (elapsed < msPerHour) {
+ return `${Math.round(elapsed / msPerMinute)} minutes ago`;
+ } else if (elapsed < msPerDay) {
+ return `${Math.round(elapsed / msPerHour)} hours ago`;
+ } else if (elapsed < msPerWeek) {
+ return `${Math.round(elapsed / msPerDay)} days ago`;
+ } else if (elapsed < msPerMonth) {
+ return `${Math.round(elapsed / msPerWeek)} weeks ago`;
+ } else if (elapsed < msPerYear) {
+ return `${Math.round(elapsed / msPerMonth)} months ago`;
+ } else {
+ return `${Math.round(elapsed / msPerYear)} years ago`;
+ }
+ }
+}
diff --git a/frontend/src/app/upload/upload.component.ts b/frontend/src/app/upload/upload.component.ts
index df1cce8..a62099a 100644
--- a/frontend/src/app/upload/upload.component.ts
+++ b/frontend/src/app/upload/upload.component.ts
@@ -97,7 +97,7 @@ export class UploadComponent {
}
};
- axios.post(this.developmentStore.getBaseUrl() + 'api/v1/upload', formData, config)
+ axios.post(this.developmentStore.getBaseUrl() + 'api/v1/public/upload', formData, config)
.then(response => {
console.log('Upload completed successfully!');
console.log(response.data);
@@ -119,7 +119,7 @@ export class UploadComponent {
passwordUrlPart = `&password=${fileDetails.password}`;
}
const downloadUrl = `${baseUrl}download?fileId=${fileId}${passwordUrlPart}`;
- const deleteUrl = `${baseUrl}api/v1/deletefile?fileId=${fileId}${passwordUrlPart}`;
+ const deleteUrl = `${baseUrl}api/v1/public/deletefile?fileId=${fileId}${passwordUrlPart}`;
const statusUrl = `${baseUrl}status?fileId=${fileId}${passwordUrlPart}`;
return {
downloadUrl,
@@ -151,7 +151,7 @@ export class UploadComponent {
axios({
method: 'post',
- url: this.developmentStore.getBaseUrl() + 'api/v1/upload-speed-test',
+ url: this.developmentStore.getBaseUrl() + 'api/v1/public/upload-speed-test',
data: uint8View,
headers: {
'Content-Type': 'application/octet-stream',
diff --git a/frontend/src/store/authStore.ts b/frontend/src/store/authStore.ts
new file mode 100644
index 0000000..577ad1a
--- /dev/null
+++ b/frontend/src/store/authStore.ts
@@ -0,0 +1,40 @@
+import { Injectable } from '@angular/core';
+import { BehaviorSubject } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+interface AuthStoreState {
+ username: string;
+ token: string;
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class AuthStore {
+ private state: BehaviorSubject = new BehaviorSubject({
+ username: "",
+ token: "",
+ });
+
+ // Getter for username
+ get username$() {
+ return this.state.asObservable().pipe(map(state => state.username));
+ }
+
+ // Getter for token
+ get token$() {
+ return this.state.asObservable().pipe(map(state => state.token));
+ }
+
+ // Mutation for username
+ setUsername(username: string) {
+ const currentState = this.state.getValue();
+ this.state.next({ ...currentState, username });
+ }
+
+ // Mutation for token
+ setToken(token: string) {
+ const currentState = this.state.getValue();
+ this.state.next({ ...currentState, token });
+ }
+}
diff --git a/src/main/java/de/w665/sharepulse/SharepulseApplication.java b/src/main/java/de/w665/sharepulse/SharepulseApplication.java
index 2c4ac58..2f2af61 100644
--- a/src/main/java/de/w665/sharepulse/SharepulseApplication.java
+++ b/src/main/java/de/w665/sharepulse/SharepulseApplication.java
@@ -3,10 +3,15 @@ package de.w665.sharepulse;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import java.util.Date;
+
@SpringBootApplication
public class SharepulseApplication {
+ public static Date startTime;
+
public static void main(String[] args) {
+ startTime = new Date();
SpringApplication.run(SharepulseApplication.class, args);
}
diff --git a/src/main/java/de/w665/sharepulse/config/CorsConfig.java b/src/main/java/de/w665/sharepulse/config/CorsConfig.java
index 37f5971..09468b7 100644
--- a/src/main/java/de/w665/sharepulse/config/CorsConfig.java
+++ b/src/main/java/de/w665/sharepulse/config/CorsConfig.java
@@ -17,6 +17,7 @@ public class CorsConfig implements WebMvcConfigurer {
registry.addMapping("/api/v1/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
- .allowedHeaders("*");
+ .allowedHeaders("*")
+ .maxAge(3600);
}
}
diff --git a/src/main/java/de/w665/sharepulse/config/CustomAuthenticationFilter.java b/src/main/java/de/w665/sharepulse/config/CustomAuthenticationFilter.java
new file mode 100644
index 0000000..3defc5a
--- /dev/null
+++ b/src/main/java/de/w665/sharepulse/config/CustomAuthenticationFilter.java
@@ -0,0 +1,17 @@
+package de.w665.sharepulse.config;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import org.springframework.web.filter.GenericFilterBean;
+
+import java.io.IOException;
+
+public class CustomAuthenticationFilter extends GenericFilterBean {
+ @Override
+ public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
+ // Custom logic here
+ filterChain.doFilter(servletRequest, servletResponse);
+ }
+}
diff --git a/src/main/java/de/w665/sharepulse/config/MvcConfig.java b/src/main/java/de/w665/sharepulse/config/MvcConfig.java
index 7e1d869..3e3aac3 100644
--- a/src/main/java/de/w665/sharepulse/config/MvcConfig.java
+++ b/src/main/java/de/w665/sharepulse/config/MvcConfig.java
@@ -16,6 +16,8 @@ public class MvcConfig implements WebMvcConfigurer {
registry.addViewController("/upload").setViewName("forward:/index.html");
registry.addViewController("/credits").setViewName("forward:/index.html");
registry.addViewController("/licenses").setViewName("forward:/index.html");
+ registry.addViewController("/login").setViewName("forward:/index.html");
+ registry.addViewController("/secure/administration").setViewName("forward:/index.html");
}
}
diff --git a/src/main/java/de/w665/sharepulse/config/SecurityConfig.java b/src/main/java/de/w665/sharepulse/config/SecurityConfig.java
new file mode 100644
index 0000000..de39681
--- /dev/null
+++ b/src/main/java/de/w665/sharepulse/config/SecurityConfig.java
@@ -0,0 +1,54 @@
+package de.w665.sharepulse.config;
+
+import de.w665.sharepulse.rest.security.JwtAuthenticationFilter;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+@Configuration
+@EnableWebSecurity
+public class SecurityConfig {
+
+ private final JwtAuthenticationFilter jwtAuthenticationFilter;
+
+ public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
+ this.jwtAuthenticationFilter = jwtAuthenticationFilter;
+ }
+
+ // This bean is required for Spring Security, though it's not used in this project
+ // Prevents Spring from generating a default password
+ @Bean
+ UserDetailsService emptyDetailsService() {
+ return username -> { throw new UsernameNotFoundException("no local users, only JWT tokens allowed"); };
+ }
+
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ http
+ .csrf(csrf -> csrf.ignoringRequestMatchers("/api/v1/**")) // Disable CSRF for API routes
+ .sessionManagement(sessionManagement -> sessionManagement
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // No session will be created by Spring Security
+ )
+ .authorizeHttpRequests(authorize -> authorize
+ .requestMatchers("/api/v1/secure/**").authenticated() // Secure these endpoints
+ .anyRequest().permitAll() // All other requests are allowed without authentication
+ )
+ .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // Apply JWT filter
+ .logout(LogoutConfigurer::permitAll)
+ .rememberMe(Customizer.withDefaults());
+
+ return http.build();
+ }
+
+
+}
+
+// TODO: Fix the security configuration to allow public access to unsecured endpoints
diff --git a/src/main/java/de/w665/sharepulse/db/RethinkDBService.java b/src/main/java/de/w665/sharepulse/db/RethinkDBService.java
index da1a8d0..b065186 100644
--- a/src/main/java/de/w665/sharepulse/db/RethinkDBService.java
+++ b/src/main/java/de/w665/sharepulse/db/RethinkDBService.java
@@ -3,33 +3,44 @@ package de.w665.sharepulse.db;
import com.rethinkdb.RethinkDB;
import com.rethinkdb.gen.exc.ReqlOpFailedError;
import com.rethinkdb.net.Connection;
+import de.w665.sharepulse.db.repo.UserRepository;
+import de.w665.sharepulse.model.User;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
+import java.util.Optional;
+
@Slf4j
@Service
public class RethinkDBService {
private final RethinkDBConfig config;
-
private final RethinkDB r;
private final Connection connection;
+ private final UserRepository userRepository;
@Value("${sharepulse.auto-reset-on-startup}")
private boolean autoResetOnStartup;
+ @Value("${sharepulse.management.user.username}")
+ private String defaultUsername;
+ @Value("${sharepulse.management.user.password}")
+ private String defaultPassword;
+
+ private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
@Autowired
- public RethinkDBService(RethinkDBConfig config, RethinkDBConnector connector) {
+ public RethinkDBService(RethinkDBConfig config, RethinkDBConnector connector, UserRepository userRepository) {
this.config = config;
-
// mapping to private vars for easier access
this.r = connector.getR();
this.connection = connector.getConnection();
+ this.userRepository = userRepository;
}
@PostConstruct
@@ -81,9 +92,59 @@ public class RethinkDBService {
log.debug("Table 'expired_file_uploads' cleared successfully.");
}
}
+
+ // rethinkdb check if table users exists
+ try {
+ r.db(config.getDatabase()).tableCreate("users").run(connection).stream();
+ log.debug("Table 'users' created successfully.");
+ } catch (ReqlOpFailedError e) {
+ log.debug("Table 'users' already exists.");
+ if(autoResetOnStartup) {
+ log.debug("Clearing content...");
+ r.db(config.getDatabase()).table("users").delete().run(connection);
+ log.debug("Table 'users' cleared successfully.");
+ }
+ }
+
+ // rethinkdb check if table user_logins exists
+ try {
+ r.db(config.getDatabase()).tableCreate("user_logins").run(connection).stream();
+ log.debug("Table 'user_logins' created successfully.");
+ } catch (ReqlOpFailedError e) {
+ log.debug("Table 'user_logins' already exists.");
+ if(autoResetOnStartup) {
+ log.debug("Clearing content...");
+ r.db(config.getDatabase()).table("user_logins").delete().run(connection);
+ log.debug("Table 'user_logins' cleared successfully.");
+ }
+ } finally {
+ try {
+ r.db(config.getDatabase()).table("user_logins").indexCreate("loginTime").run(connection);
+ log.debug("Secondary index 'loginTime' on table 'user_logins' successfully created.");
+ } catch (ReqlOpFailedError e) {
+ log.debug("Secondary index 'loginTime' already exists.");
+ } finally {
+ r.db(config.getDatabase()).table("user_logins").indexWait("loginTime").run(connection);
+ }
+ }
+
+ initializeAdminUser();
+
log.info("Database ready for operation!");
}
+ private void initializeAdminUser() {
+ Optional adminUser = userRepository.retrieveUserByUsername("admin");
+ if(adminUser.isEmpty()) {
+ User user = new User();
+ user.setUsername(defaultUsername);
+ user.setPassword(passwordEncoder.encode(defaultPassword));
+ user.setRole("ADMIN");
+ userRepository.insertUser(user);
+ log.debug("Admin user created with default credentials. Username: admin, Password: admin");
+ }
+ }
+
@PreDestroy
public void close() {
if (connection != null) {
diff --git a/src/main/java/de/w665/sharepulse/db/repo/ExpiredFileUploadRepository.java b/src/main/java/de/w665/sharepulse/db/repo/ExpiredFileUploadRepository.java
index dae75ce..d211bf3 100644
--- a/src/main/java/de/w665/sharepulse/db/repo/ExpiredFileUploadRepository.java
+++ b/src/main/java/de/w665/sharepulse/db/repo/ExpiredFileUploadRepository.java
@@ -11,6 +11,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.lang.reflect.Type;
+import java.util.List;
import java.util.Map;
@Repository
@@ -38,4 +39,17 @@ public class ExpiredFileUploadRepository {
r.db("sharepulse").table("expired_file_uploads").insert(map).run(connection);
}
+
+ public void deleteExpiredFileUpload(FileUpload fileUpload) {
+ r.db("sharepulse").table("expired_file_uploads")
+ .filter(r.hashMap("fileId", fileUpload.getFileId()))
+ .delete()
+ .run(connection);
+ }
+
+ public List findAll() {
+ return r.db("sharepulse").table("expired_file_uploads")
+ .run(connection, FileUpload.class)
+ .toList();
+ }
}
diff --git a/src/main/java/de/w665/sharepulse/db/repo/FileUploadRepository.java b/src/main/java/de/w665/sharepulse/db/repo/FileUploadRepository.java
index d68b595..27db86b 100644
--- a/src/main/java/de/w665/sharepulse/db/repo/FileUploadRepository.java
+++ b/src/main/java/de/w665/sharepulse/db/repo/FileUploadRepository.java
@@ -36,7 +36,7 @@ public class FileUploadRepository {
Type type = new TypeToken