Compare commits
10 Commits
c069028bd5
...
0f64322af3
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0f64322af3 | ||
e5ce27adfd | |||
4134367350 | |||
f65a0d0e22 | |||
e73a2e6e8d | |||
6f550c0787 | |||
8b8d9d902b | |||
5f3304674f | |||
1462e141ad | |||
1e464f6fb8 |
@ -5,7 +5,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = 'de.w665'
|
group = 'de.w665'
|
||||||
version = '1.1.2'
|
version = '1.2.1'
|
||||||
|
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = '21'
|
sourceCompatibility = '21'
|
||||||
@ -39,11 +39,11 @@ dependencies {
|
|||||||
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security
|
// 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'
|
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '3.2.4'
|
||||||
// https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api
|
// https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api
|
||||||
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.12.5'
|
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.12.6'
|
||||||
// https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl
|
// https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl
|
||||||
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.12.5'
|
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.12.6'
|
||||||
// https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-orgjson
|
// https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-orgjson
|
||||||
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-orgjson', version: '0.12.5'
|
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-orgjson', version: '0.12.6'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div class="container mx-auto p-4 mt-4">
|
<div class="container mx-auto p-4 mt-4 animate-in fade-in slide-in-from-bottom duration-500">
|
||||||
<h1 class="text-5xl font-bold text-center text-gray-800 mb-10">Admin Dashboard</h1>
|
<h1 class="text-5xl font-bold text-center text-gray-800 mb-10">Admin Dashboard</h1>
|
||||||
|
|
||||||
<!-- Statistics Section -->
|
<!-- Statistics Section -->
|
||||||
@ -54,11 +54,13 @@
|
|||||||
Change Administrator Login
|
Change Administrator Login
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary" (click)="confirm('Are you sure?') && deleteAllFileUploads()">Delete All Uploaded Files</button>
|
<button class="btn btn-secondary" (click)="confirm('Are you sure?') && deleteAllFileUploads()">Delete All Uploaded Files</button>
|
||||||
<button class="btn btn-accent">
|
<button class="btn btn-accent" (click)="openLoginHistoryModal()">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-people" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clock-history" viewBox="0 0 16 16">
|
||||||
<path d="M15 14s1 0 1-1-1-4-5-4-5 3-5 4 1 1 1 1zm-7.978-1L7 12.996c.001-.264.167-1.03.76-1.72C8.312 10.629 9.282 10 11 10c1.717 0 2.687.63 3.24 1.276.593.69.758 1.457.76 1.72l-.008.002-.014.002zM11 7a2 2 0 1 0 0-4 2 2 0 0 0 0 4m3-2a3 3 0 1 1-6 0 3 3 0 0 1 6 0M6.936 9.28a6 6 0 0 0-1.23-.247A7 7 0 0 0 5 9c-4 0-5 3-5 4q0 1 1 1h4.216A2.24 2.24 0 0 1 5 13c0-1.01.377-2.042 1.09-2.904.243-.294.526-.569.846-.816M4.92 10A5.5 5.5 0 0 0 4 13H1c0-.26.164-1.03.76-1.724.545-.636 1.492-1.256 3.16-1.275ZM1.5 5.5a3 3 0 1 1 6 0 3 3 0 0 1-6 0m3-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4"/>
|
<path d="M8.515 1.019A7 7 0 0 0 8 1V0a8 8 0 0 1 .589.022zm2.004.45a7 7 0 0 0-.985-.299l.219-.976q.576.129 1.126.342zm1.37.71a7 7 0 0 0-.439-.27l.493-.87a8 8 0 0 1 .979.654l-.615.789a7 7 0 0 0-.418-.302zm1.834 1.79a7 7 0 0 0-.653-.796l.724-.69q.406.429.747.91zm.744 1.352a7 7 0 0 0-.214-.468l.893-.45a8 8 0 0 1 .45 1.088l-.95.313a7 7 0 0 0-.179-.483m.53 2.507a7 7 0 0 0-.1-1.025l.985-.17q.1.58.116 1.17zm-.131 1.538q.05-.254.081-.51l.993.123a8 8 0 0 1-.23 1.155l-.964-.267q.069-.247.12-.501m-.952 2.379q.276-.436.486-.908l.914.405q-.24.54-.555 1.038zm-.964 1.205q.183-.183.35-.378l.758.653a8 8 0 0 1-.401.432z"/>
|
||||||
|
<path d="M8 1a7 7 0 1 0 4.95 11.95l.707.707A8.001 8.001 0 1 1 8 0z"/>
|
||||||
|
<path d="M7.5 3a.5.5 0 0 1 .5.5v5.21l3.248 1.856a.5.5 0 0 1-.496.868l-3.5-2A.5.5 0 0 1 7 9V3.5a.5.5 0 0 1 .5-.5"/>
|
||||||
</svg>
|
</svg>
|
||||||
User Management
|
Login History
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-neutral" (click)="logout()">
|
<button class="btn btn-neutral" (click)="logout()">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-box-arrow-left" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-box-arrow-left" viewBox="0 0 16 16">
|
||||||
@ -98,7 +100,16 @@
|
|||||||
<td>{{ file.fileName }}</td>
|
<td>{{ file.fileName }}</td>
|
||||||
<td>{{ file.fileSize | formatFileSizePipe }}</td>
|
<td>{{ file.fileSize | formatFileSizePipe }}</td>
|
||||||
<td>{{ file.singleDownload ? 'true' : 'false' }}</td>
|
<td>{{ file.singleDownload ? 'true' : 'false' }}</td>
|
||||||
<td>{{ file.disabled ? 'true' : 'false' }}</td>
|
<td>
|
||||||
|
<div class="flex flex-row justify-center">
|
||||||
|
{{ file.disabled ? 'true' : 'false' }}
|
||||||
|
<button class="ms-2 btn btn-xs" (click)="disableFile(file.fileId)">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-ban" viewBox="0 0 16 16">
|
||||||
|
<path d="M15 8a6.97 6.97 0 0 0-1.71-4.584l-9.874 9.875A7 7 0 0 0 15 8M2.71 12.584l9.874-9.875a7 7 0 0 0-9.874 9.874ZM16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td>{{ file.uploadDate | date: 'medium' }}</td>
|
<td>{{ file.uploadDate | date: 'medium' }}</td>
|
||||||
<td>{{ file.uploadedByIpAddress }}</td>
|
<td>{{ file.uploadedByIpAddress }}</td>
|
||||||
<td>{{ file.downloadCount }}</td>
|
<td>{{ file.downloadCount }}</td>
|
||||||
@ -153,3 +164,10 @@
|
|||||||
></app-edituser>
|
></app-edituser>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
<dialog #login_history_modal class="modal">
|
||||||
|
<div class="modal-box w-11/12 max-w-5xl">
|
||||||
|
<app-loginhistory>
|
||||||
|
[username]="username"
|
||||||
|
</app-loginhistory>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
@ -10,6 +10,7 @@ import {DurationPipe} from "../duration.pipe";
|
|||||||
import {RelativeTimePipe} from "../relative-time.pipe";
|
import {RelativeTimePipe} from "../relative-time.pipe";
|
||||||
import {FormsModule} from "@angular/forms";
|
import {FormsModule} from "@angular/forms";
|
||||||
import {EdituserComponent} from "./edituser/edituser.component";
|
import {EdituserComponent} from "./edituser/edituser.component";
|
||||||
|
import {LoginhistoryComponent} from "./loginhistory/loginhistory.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-adminui',
|
selector: 'app-adminui',
|
||||||
@ -23,7 +24,8 @@ import {EdituserComponent} from "./edituser/edituser.component";
|
|||||||
RelativeTimePipe,
|
RelativeTimePipe,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
EdituserComponent,
|
EdituserComponent,
|
||||||
NgIf
|
NgIf,
|
||||||
|
LoginhistoryComponent
|
||||||
],
|
],
|
||||||
templateUrl: './adminui.component.html',
|
templateUrl: './adminui.component.html',
|
||||||
styleUrl: './adminui.component.scss'
|
styleUrl: './adminui.component.scss'
|
||||||
@ -31,6 +33,7 @@ import {EdituserComponent} from "./edituser/edituser.component";
|
|||||||
export class AdminuiComponent {
|
export class AdminuiComponent {
|
||||||
|
|
||||||
@ViewChild('edit_user_modal') edit_user_modal: ElementRef<HTMLDialogElement> | undefined;
|
@ViewChild('edit_user_modal') edit_user_modal: ElementRef<HTMLDialogElement> | undefined;
|
||||||
|
@ViewChild('login_history_modal') login_history_modal: ElementRef<HTMLDialogElement> | undefined;
|
||||||
|
|
||||||
fileUploads: any[] = [];
|
fileUploads: any[] = [];
|
||||||
expiredFileUploads: any[] = [];
|
expiredFileUploads: any[] = [];
|
||||||
@ -83,6 +86,10 @@ export class AdminuiComponent {
|
|||||||
this.edit_user_modal?.nativeElement.showModal();
|
this.edit_user_modal?.nativeElement.showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openLoginHistoryModal() {
|
||||||
|
this.login_history_modal?.nativeElement.showModal();
|
||||||
|
}
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
this.authStore.setToken("");
|
this.authStore.setToken("");
|
||||||
this.authStore.setUsername("");
|
this.authStore.setUsername("");
|
||||||
@ -130,7 +137,7 @@ export class AdminuiComponent {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.statistics = response.data;
|
this.statistics = response.data;
|
||||||
console.log(this.statistics)
|
//console.log(this.statistics)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
@ -145,7 +152,26 @@ export class AdminuiComponent {
|
|||||||
'Authorization': 'Bearer ' + await firstValueFrom(this.authStore.token$)
|
'Authorization': 'Bearer ' + await firstValueFrom(this.authStore.token$)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log(this.statistics)
|
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) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
<form method="dialog">
|
||||||
|
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||||
|
</form>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<form>
|
<form>
|
||||||
<h3 class="font-bold text-lg mb-10">Edit your ({{parsedUsername}}) account details</h3>
|
<h3 class="font-bold text-lg mb-10">Edit your ({{parsedUsername}}) account details</h3>
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
<form method="dialog">
|
||||||
|
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||||
|
</form>
|
||||||
|
<h2 class="mb-3">Operations</h2>
|
||||||
|
<button class="btn btn-error" (click)="deleteLogins()">Delete all stored logins for the current user</button>
|
||||||
|
<h2 class="mt-5 mb-3">Login history</h2>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<!-- <th>ID</th>-->
|
||||||
|
<!-- <th>User ID</th>-->
|
||||||
|
<th>Login Time</th>
|
||||||
|
<th>Login IP</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let entry of loginHistory">
|
||||||
|
<!-- <td>{{ entry.id }}</td>-->
|
||||||
|
<!-- <td>{{ entry.userId }}</td>-->
|
||||||
|
<td>{{ entry.loginTime | date: 'dd. MMMM yyyy, HH:mm:ss' }}</td>
|
||||||
|
<td>{{ entry.loginIp }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LoginhistoryComponent } from './loginhistory.component';
|
||||||
|
|
||||||
|
describe('LoginhistoryComponent', () => {
|
||||||
|
let component: LoginhistoryComponent;
|
||||||
|
let fixture: ComponentFixture<LoginhistoryComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [LoginhistoryComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(LoginhistoryComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -42,9 +42,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button class="btn mb-10" routerLink="/login">Administration</button>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<span class="text-indigo-600 cursor-pointer" (click)="openPrivacyPolicyModal()">Privacy Policy</span> |
|
<span class="text-indigo-600 cursor-pointer" (click)="openPrivacyPolicyModal()">Privacy Policy</span> |
|
||||||
<span class="text-indigo-600 cursor-pointer" (click)="openTermsOfUseModal()">Terms of Use</span>
|
<span class="text-indigo-600 cursor-pointer" (click)="openTermsOfUseModal()">Terms of Use</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<p class="text-gray-800 mt-10">© 2024 SharePulse. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class="container mx-auto p-4 mt-20">
|
<div class="container mx-auto p-4 mt-20">
|
||||||
<div class="bg-white shadow-lg rounded-lg p-10 w-full max-w-xl mx-auto">
|
<div class="bg-white shadow-lg rounded-lg p-10 w-full max-w-xl mx-auto">
|
||||||
<h2 class="text-3xl font-bold text-gray-800 mb-6 text-center">Login to SharePulse</h2>
|
<h2 class="text-3xl font-bold text-gray-800 mb-10 text-center">Login to SharePulse</h2>
|
||||||
<form>
|
<form class="mb-10">
|
||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
<label class="block text-gray-700 text-sm font-bold mb-2 text-center" for="username">
|
<label class="block text-gray-700 text-sm font-bold mb-2 text-center" for="username">
|
||||||
Username
|
Username
|
||||||
@ -17,10 +17,10 @@
|
|||||||
<input class="input w-full shadow text-center" id="password" type="password" placeholder="********"
|
<input class="input w-full shadow text-center" id="password" type="password" placeholder="********"
|
||||||
[(ngModel)]="inputPassword" name="password"
|
[(ngModel)]="inputPassword" name="password"
|
||||||
[ngClass]="{'input-error': loginFailed}"
|
[ngClass]="{'input-error': loginFailed}"
|
||||||
(keydown.enter)="tryToLogin()" >
|
(keydown.enter)="tryToLogin()">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-5">
|
<div class="mb-10">
|
||||||
<label class="flex items-center justify-center">
|
<label class="flex items-center justify-center tooltip" data-tip="Storing login sessions is not functional yet">
|
||||||
<input type="checkbox" class="toggle" checked name="keepSignedIn"/>
|
<input type="checkbox" class="toggle" checked name="keepSignedIn"/>
|
||||||
<span class="ml-2 text-gray-700">Keep me signed in</span>
|
<span class="ml-2 text-gray-700">Keep me signed in</span>
|
||||||
</label>
|
</label>
|
||||||
|
@ -49,14 +49,10 @@ export class LoginComponent {
|
|||||||
this.authStore.setToken(response.data.token);
|
this.authStore.setToken(response.data.token);
|
||||||
this.authStore.setUsername(this.inputUsername);
|
this.authStore.setUsername(this.inputUsername);
|
||||||
|
|
||||||
console.log("Login successful");
|
|
||||||
console.log("Token: " + await firstValueFrom(this.authStore.token$));
|
|
||||||
console.log("Username: " + await firstValueFrom(this.authStore.username$));
|
|
||||||
|
|
||||||
//timeout
|
//timeout
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.router.navigate(['/secure/administration']);
|
this.router.navigate(['/secure/administration']);
|
||||||
}, 1000);
|
}, 500);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
@ -12,6 +12,8 @@ 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.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
@ -52,7 +54,12 @@ public class FileUploadRepository {
|
|||||||
} catch (NoSuchElementException e) {
|
} catch (NoSuchElementException e) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FileUpload> findAll() {
|
||||||
|
return r.db("sharepulse").table("file_uploads")
|
||||||
|
.run(connection, FileUpload.class)
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateFileUpload(FileUpload updatedFileUpload) {
|
public void updateFileUpload(FileUpload updatedFileUpload) {
|
||||||
@ -79,32 +86,24 @@ public class FileUploadRepository {
|
|||||||
.run(connection);
|
.run(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This query filters all file uploads that are older than 24 hours from the file_uplaods table (not the expired_file_uploads table)
|
public List<FileUpload> getAllExpiredFileUploads() {
|
||||||
public List<FileUpload> findAllExpiredActiveFileUploads() {
|
long timestamp = getTimestamp24HoursAgo();
|
||||||
|
|
||||||
long timestamp = getOneMinuteAgoTimestamp();
|
|
||||||
|
|
||||||
return r.db("sharepulse").table("file_uploads")
|
return r.db("sharepulse").table("file_uploads")
|
||||||
.filter(row -> row.g("uploadDate").lt(timestamp))
|
.filter(row -> row.g("uploadDate").lt(timestamp))
|
||||||
.run(connection, FileUpload.class)
|
.run(connection, FileUpload.class)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FileUpload> findAll() {
|
private long getTimestamp24HoursAgo() {
|
||||||
return r.db("sharepulse").table("file_uploads")
|
Instant now = Instant.now();
|
||||||
.run(connection, FileUpload.class)
|
Instant oneMinuteAgo = now.minus(24, ChronoUnit.HOURS);
|
||||||
.toList();
|
return oneMinuteAgo.toEpochMilli();
|
||||||
}
|
}
|
||||||
|
|
||||||
private long get24HoursAgoTimestamp() {
|
// For testing only
|
||||||
Calendar calendar = Calendar.getInstance();
|
private long getTimestampOneMinuteAgo() {
|
||||||
calendar.add(Calendar.HOUR, -24);
|
Instant now = Instant.now();
|
||||||
return calendar.getTimeInMillis() / 1000;
|
Instant oneMinuteAgo = now.minus(1, ChronoUnit.MINUTES);
|
||||||
|
return oneMinuteAgo.toEpochMilli();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -12,6 +12,8 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
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.util.List;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Repository
|
@Repository
|
||||||
public class UserLoginRepository {
|
public class UserLoginRepository {
|
||||||
@ -47,4 +49,19 @@ public class UserLoginRepository {
|
|||||||
// Return the second most recent login if exists
|
// Return the second most recent login if exists
|
||||||
return result.hasNext() ? result.next() : null;
|
return result.hasNext() ? result.next() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<UserLogin> getUserLogins(String userId) {
|
||||||
|
Result<UserLogin> result = r.db(config.getDatabase()).table(TABLE_NAME)
|
||||||
|
.orderBy().optArg("index", r.desc("loginTime"))
|
||||||
|
.filter(r.hashMap("userId", userId))
|
||||||
|
.run(connection, UserLogin.class);
|
||||||
|
return result.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteAllUserLogins(String userId) {
|
||||||
|
r.db(config.getDatabase()).table(TABLE_NAME)
|
||||||
|
.filter(r.hashMap("userId", userId))
|
||||||
|
.delete()
|
||||||
|
.run(connection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
package de.w665.sharepulse.rest.mappings;
|
package de.w665.sharepulse.rest.mappings;
|
||||||
|
|
||||||
import de.w665.sharepulse.SharepulseApplication;
|
import de.w665.sharepulse.SharepulseApplication;
|
||||||
|
import de.w665.sharepulse.db.repo.FileUploadRepository;
|
||||||
import de.w665.sharepulse.db.repo.UserLoginRepository;
|
import de.w665.sharepulse.db.repo.UserLoginRepository;
|
||||||
import de.w665.sharepulse.db.repo.UserRepository;
|
import de.w665.sharepulse.db.repo.UserRepository;
|
||||||
import de.w665.sharepulse.model.FileUpload;
|
import de.w665.sharepulse.model.FileUpload;
|
||||||
import de.w665.sharepulse.model.User;
|
import de.w665.sharepulse.model.User;
|
||||||
import de.w665.sharepulse.rest.SecureApiRestController;
|
import de.w665.sharepulse.rest.SecureApiRestController;
|
||||||
|
import de.w665.sharepulse.rest.ro.FileDeleteRequest;
|
||||||
import de.w665.sharepulse.rest.ro.UserEditRequest;
|
import de.w665.sharepulse.rest.ro.UserEditRequest;
|
||||||
import de.w665.sharepulse.service.AuthenticationService;
|
import de.w665.sharepulse.service.AuthenticationService;
|
||||||
import de.w665.sharepulse.service.FileCleanupService;
|
import de.w665.sharepulse.service.FileCleanupService;
|
||||||
@ -27,12 +29,14 @@ public class Administration extends SecureApiRestController {
|
|||||||
private final AuthenticationService authenticationService;
|
private final AuthenticationService authenticationService;
|
||||||
private final FileCleanupService fileCleanupService;
|
private final FileCleanupService fileCleanupService;
|
||||||
private final UserLoginRepository userLoginRepository;
|
private final UserLoginRepository userLoginRepository;
|
||||||
|
private final FileUploadRepository fileUploadRepository;
|
||||||
|
|
||||||
public Administration(UserRepository userRepository, AuthenticationService authenticationService, FileCleanupService fileCleanupService, UserLoginRepository userLoginRepository) {
|
public Administration(UserRepository userRepository, AuthenticationService authenticationService, FileCleanupService fileCleanupService, UserLoginRepository userLoginRepository, FileUploadRepository fileUploadRepository) {
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.authenticationService = authenticationService;
|
this.authenticationService = authenticationService;
|
||||||
this.fileCleanupService = fileCleanupService;
|
this.fileCleanupService = fileCleanupService;
|
||||||
this.userLoginRepository = userLoginRepository;
|
this.userLoginRepository = userLoginRepository;
|
||||||
|
this.fileUploadRepository = fileUploadRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/statistics")
|
@GetMapping("/statistics")
|
||||||
@ -75,9 +79,48 @@ public class Administration extends SecureApiRestController {
|
|||||||
return ResponseEntity.ok(user);
|
return ResponseEntity.ok(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/loginhistory")
|
||||||
|
public ResponseEntity<Object> getLoginHistory(HttpServletRequest request) {
|
||||||
|
String token = request.getHeader("Authorization");
|
||||||
|
token = token.substring(7);
|
||||||
|
String username = authenticationService.getClaimValue(token, "username", String.class);
|
||||||
|
Optional<User> optionalUser = userRepository.retrieveUserByUsername(username);
|
||||||
|
if(optionalUser.isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest().body("User not found");
|
||||||
|
}
|
||||||
|
String userId = optionalUser.get().getId();
|
||||||
|
return ResponseEntity.ok(userLoginRepository.getUserLogins(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/loginhistory")
|
||||||
|
public ResponseEntity<Object> deleteLoginHistory(HttpServletRequest request) {
|
||||||
|
String token = request.getHeader("Authorization");
|
||||||
|
token = token.substring(7);
|
||||||
|
String username = authenticationService.getClaimValue(token, "username", String.class);
|
||||||
|
Optional<User> optionalUser = userRepository.retrieveUserByUsername(username);
|
||||||
|
if(optionalUser.isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest().body("User not found");
|
||||||
|
}
|
||||||
|
String userId = optionalUser.get().getId();
|
||||||
|
userLoginRepository.deleteAllUserLogins(userId);
|
||||||
|
return ResponseEntity.ok("User logins deleted successfully!");
|
||||||
|
}
|
||||||
|
|
||||||
@DeleteMapping("/files")
|
@DeleteMapping("/files")
|
||||||
public ResponseEntity<Object> deleteFiles(HttpServletRequest request) {
|
public ResponseEntity<Object> deleteFiles(HttpServletRequest request) {
|
||||||
List<FileUpload> files = fileCleanupService.deleteFiles();
|
List<FileUpload> files = fileCleanupService.deleteFiles();
|
||||||
return ResponseEntity.ok(files);
|
return ResponseEntity.ok(files);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PutMapping("/files/disable")
|
||||||
|
public ResponseEntity<Object> disableFileUploads(@RequestBody FileDeleteRequest fdr, HttpServletRequest request) {
|
||||||
|
Optional<FileUpload> optionalFileUpload = fileUploadRepository.retrieveFileUploadByFileId(fdr.getFileId());
|
||||||
|
if(optionalFileUpload.isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest().body("File not found");
|
||||||
|
}
|
||||||
|
FileUpload fileUpload = optionalFileUpload.get();
|
||||||
|
fileUpload.setDisabled(true);
|
||||||
|
fileUploadRepository.updateFileUpload(fileUpload);
|
||||||
|
return ResponseEntity.ok("File " + fdr.getFileId() + " disabled successfully.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
package de.w665.sharepulse.rest.ro;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@ToString
|
||||||
|
public class FileDeleteRequest {
|
||||||
|
private String fileId;
|
||||||
|
}
|
@ -23,10 +23,11 @@ public class FileCleanupService {
|
|||||||
this.fileService = fileService;
|
this.fileService = fileService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//@Scheduled(cron = "*/10 * * * * *") // every 10 seconds
|
||||||
@Scheduled(cron = "0 0 * * * *")
|
@Scheduled(cron = "0 0 * * * *")
|
||||||
public void cleanup() {
|
public void cleanup() {
|
||||||
log.debug("Running cleanup...");
|
log.debug("Running cleanup...");
|
||||||
List<FileUpload> expFileUploads = fileUploadRepository.findAllExpiredActiveFileUploads();
|
List<FileUpload> expFileUploads = fileUploadRepository.getAllExpiredFileUploads();
|
||||||
for (FileUpload fileUpload : expFileUploads) {
|
for (FileUpload fileUpload : expFileUploads) {
|
||||||
fileService.deleteFile(fileUpload);
|
fileService.deleteFile(fileUpload);
|
||||||
expiredFileUploadRepository.insertExpiredFileUpload(fileUpload);
|
expiredFileUploadRepository.insertExpiredFileUpload(fileUpload);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user