Compare commits
No commits in common. "main" and "v1.1.1" have entirely different histories.
@ -66,18 +66,16 @@ jobs:
|
|||||||
- name: Initialize Docker runtime
|
- name: Initialize Docker runtime
|
||||||
if: env.SKIP_SUBSEQUENT_STEPS != 'true'
|
if: env.SKIP_SUBSEQUENT_STEPS != 'true'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
apt-get update
|
||||||
sudo apt-get install ca-certificates curl
|
apt-get install ca-certificates curl gnupg lsb-release -y
|
||||||
sudo install -m 0755 -d /etc/apt/keyrings
|
install -m 0755 -d /etc/apt/keyrings
|
||||||
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||||
sudo chmod a+r /etc/apt/keyrings/docker.asc
|
chmod a+r /etc/apt/keyrings/docker.gpg
|
||||||
|
|
||||||
# Add the repository to Apt sources:
|
|
||||||
echo \
|
echo \
|
||||||
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
|
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
|
||||||
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
|
$(lsb_release -cs) stable" | \
|
||||||
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
sudo apt-get update
|
apt-get update
|
||||||
apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin -y
|
apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin -y
|
||||||
|
|
||||||
- name: Download Artifact
|
- name: Download Artifact
|
||||||
|
@ -57,7 +57,4 @@ networks:
|
|||||||
volumes:
|
volumes:
|
||||||
rethinkdb_data:
|
rethinkdb_data:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Dev notes
|
|
||||||
Fix slow build times: Use Gradle Build and Run settings set to IntelliJ IDEA
|
|
11
build.gradle
11
build.gradle
@ -5,7 +5,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = 'de.w665'
|
group = 'de.w665'
|
||||||
version = '1.2.1'
|
version = '1.1.1'
|
||||||
|
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = '21'
|
sourceCompatibility = '21'
|
||||||
@ -36,15 +36,6 @@ dependencies {
|
|||||||
implementation 'com.rethinkdb:rethinkdb-driver:2.4.4'
|
implementation 'com.rethinkdb:rethinkdb-driver:2.4.4'
|
||||||
// https://mvnrepository.com/artifact/com.google.code.gson/gson
|
// https://mvnrepository.com/artifact/com.google.code.gson/gson
|
||||||
implementation group: 'com.google.code.gson', name: 'gson', version: '2.10.1'
|
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.6'
|
|
||||||
// https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl
|
|
||||||
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.12.6'
|
|
||||||
// https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-orgjson
|
|
||||||
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-orgjson', version: '0.12.6'
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bootJar {
|
bootJar {
|
||||||
|
@ -97,8 +97,5 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"cli": {
|
|
||||||
"analytics": false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
73
frontend/package-lock.json
generated
73
frontend/package-lock.json
generated
@ -5945,17 +5945,17 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.19.2",
|
"version": "4.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
||||||
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
|
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
"body-parser": "1.20.2",
|
"body-parser": "1.20.1",
|
||||||
"content-disposition": "0.5.4",
|
"content-disposition": "0.5.4",
|
||||||
"content-type": "~1.0.4",
|
"content-type": "~1.0.4",
|
||||||
"cookie": "0.6.0",
|
"cookie": "0.5.0",
|
||||||
"cookie-signature": "1.0.6",
|
"cookie-signature": "1.0.6",
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"depd": "2.0.0",
|
"depd": "2.0.0",
|
||||||
@ -5986,10 +5986,34 @@
|
|||||||
"node": ">= 0.10.0"
|
"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": {
|
"node_modules/express/node_modules/cookie": {
|
||||||
"version": "0.6.0",
|
"version": "0.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
@ -6028,6 +6052,21 @@
|
|||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/express/node_modules/statuses": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||||
@ -6239,9 +6278,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/follow-redirects": {
|
"node_modules/follow-redirects": {
|
||||||
"version": "1.15.6",
|
"version": "1.15.5",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
|
||||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@ -7037,9 +7076,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ip": {
|
"node_modules/ip": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
|
||||||
"integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==",
|
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
@ -11968,9 +12007,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": {
|
"node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": {
|
||||||
"version": "5.3.4",
|
"version": "5.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz",
|
||||||
"integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==",
|
"integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"colorette": "^2.0.10",
|
"colorette": "^2.0.10",
|
||||||
|
@ -1,173 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
|
||||||
<!-- Statistics Section -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 mb-10">
|
|
||||||
<div class="shadow stats bg-white">
|
|
||||||
<div class="stat">
|
|
||||||
<div class="stat-title">Total Files Uploaded</div>
|
|
||||||
<div class="stat-value">{{ fileUploads.length + expiredFileUploads.length }}</div>
|
|
||||||
<div class="stat-desc">Since launch</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="shadow stats bg-white">
|
|
||||||
<div class="stat">
|
|
||||||
<div class="stat-title">Total File Size on Disk</div>
|
|
||||||
<div class="stat-value">{{ totalFileSizeOnDisk | formatFileSizePipe }}</div>
|
|
||||||
<div class="stat-desc">Across all stored files</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="shadow stats bg-white">
|
|
||||||
<div class="stat">
|
|
||||||
<div class="stat-title">Operational For</div>
|
|
||||||
<div class="stat-value">{{ statistics.applicationOnlineTime | duration }}</div>
|
|
||||||
<div class="stat-desc">Since launch</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="shadow stats bg-white">
|
|
||||||
<div class="stat">
|
|
||||||
<div class="stat-title">Total Downloads</div>
|
|
||||||
<div class="stat-value">{{ totalFileDownloads }}</div>
|
|
||||||
<div class="stat-desc">All time</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="shadow stats bg-white">
|
|
||||||
<div class="stat">
|
|
||||||
<div class="stat-title">Last Admin Login</div>
|
|
||||||
<div *ngIf="statistics.lastUserLogin" class="stat-value">{{ statistics.lastUserLogin.loginTime | relativeTime }}</div>
|
|
||||||
<div *ngIf="!statistics.lastUserLogin" class="stat-value">First login...</div>
|
|
||||||
<div class="stat-desc">Most recent login</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Buttons Section -->
|
|
||||||
<div class="flex justify-center mt-10 space-x-4">
|
|
||||||
<button class="btn btn-primary" (click)="openEditUserModal()">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person" viewBox="0 0 16 16">
|
|
||||||
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6m2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0m4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4m-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10s-3.516.68-4.168 1.332c-.678.678-.83 1.418-.832 1.664z"/>
|
|
||||||
</svg>
|
|
||||||
Change Administrator Login
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary" (click)="confirm('Are you sure?') && deleteAllFileUploads()">Delete All Uploaded Files</button>
|
|
||||||
<button class="btn btn-accent" (click)="openLoginHistoryModal()">
|
|
||||||
<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="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>
|
|
||||||
Login History
|
|
||||||
</button>
|
|
||||||
<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">
|
|
||||||
<path fill-rule="evenodd" d="M6 12.5a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-9a.5.5 0 0 0-.5-.5h-8a.5.5 0 0 0-.5.5v2a.5.5 0 0 1-1 0v-2A1.5 1.5 0 0 1 6.5 2h8A1.5 1.5 0 0 1 16 3.5v9a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 5 12.5v-2a.5.5 0 0 1 1 0z"/>
|
|
||||||
<path fill-rule="evenodd" d="M.146 8.354a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L1.707 7.5H10.5a.5.5 0 0 1 0 1H1.707l2.147 2.146a.5.5 0 0 1-.708.708z"/>
|
|
||||||
</svg>
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="mt-10 mb-10">
|
|
||||||
|
|
||||||
<!-- Table Section -->
|
|
||||||
<h2>Active file uploads</h2>
|
|
||||||
<div class="w-full overflow-x-auto mt-10 mb-10">
|
|
||||||
<table class="table w-full">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Entity ID</th>
|
|
||||||
<th>File ID</th>
|
|
||||||
<th>File Name</th>
|
|
||||||
<th>File Size</th>
|
|
||||||
<th>Single Download</th>
|
|
||||||
<th>Disabled</th>
|
|
||||||
<th>Upload Date</th>
|
|
||||||
<th>Uploaded By IP</th>
|
|
||||||
<th>Download Count</th>
|
|
||||||
<!--<th>File Description</th>-->
|
|
||||||
<th>Password Protected</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let file of fileUploads">
|
|
||||||
<td>{{ file.id }}</td>
|
|
||||||
<td>{{ file.fileId }}</td>
|
|
||||||
<td>{{ file.fileName }}</td>
|
|
||||||
<td>{{ file.fileSize | formatFileSizePipe }}</td>
|
|
||||||
<td>{{ file.singleDownload ? '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.uploadedByIpAddress }}</td>
|
|
||||||
<td>{{ file.downloadCount }}</td>
|
|
||||||
<!--<td>{{ file.fileDescription }}</td>-->
|
|
||||||
<td>{{ file.passwordProtected ? 'true' : 'false' }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Expired file uploads</h2>
|
|
||||||
<div class="w-full overflow-x-auto mt-10">
|
|
||||||
<table class="table w-full">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Entity ID</th>
|
|
||||||
<th>File ID</th>
|
|
||||||
<th>File Name</th>
|
|
||||||
<th>File Size</th>
|
|
||||||
<th>Single Download</th>
|
|
||||||
<th>Disabled</th>
|
|
||||||
<th>Upload Date</th>
|
|
||||||
<th>Uploaded By IP</th>
|
|
||||||
<th>Download Count</th>
|
|
||||||
<!--<th>File Description</th>-->
|
|
||||||
<th>Password Protected</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let file of expiredFileUploads">
|
|
||||||
<td>{{ file.id }}</td>
|
|
||||||
<td>{{ file.fileId }}</td>
|
|
||||||
<td>{{ file.fileName }}</td>
|
|
||||||
<td>{{ file.fileSize | formatFileSizePipe }}</td>
|
|
||||||
<td>{{ file.singleDownload ? 'true' : 'false' }}</td>
|
|
||||||
<td>{{ file.disabled ? 'true' : 'false' }}</td>
|
|
||||||
<td>{{ file.uploadDate | date: 'medium' }}</td>
|
|
||||||
<td>{{ file.uploadedByIpAddress }}</td>
|
|
||||||
<td>{{ file.downloadCount }}</td>
|
|
||||||
<!--<td>{{ file.fileDescription }}</td>-->
|
|
||||||
<td>{{ file.passwordProtected ? 'true' : 'false' }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Modal -->
|
|
||||||
<dialog #edit_user_modal class="modal">
|
|
||||||
<div class="modal-box">
|
|
||||||
<app-edituser
|
|
||||||
[username]="username"
|
|
||||||
></app-edituser>
|
|
||||||
</div>
|
|
||||||
</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>
|
|
@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { AdminuiComponent } from './adminui.component';
|
|
||||||
|
|
||||||
describe('AdminuiComponent', () => {
|
|
||||||
let component: AdminuiComponent;
|
|
||||||
let fixture: ComponentFixture<AdminuiComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [AdminuiComponent]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(AdminuiComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,181 +0,0 @@
|
|||||||
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<HTMLDialogElement> | undefined;
|
|
||||||
@ViewChild('login_history_modal') login_history_modal: ElementRef<HTMLDialogElement> | 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;
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
<form method="dialog">
|
|
||||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
|
||||||
</form>
|
|
||||||
<div class="w-full">
|
|
||||||
<form>
|
|
||||||
<h3 class="font-bold text-lg mb-10">Edit your ({{parsedUsername}}) account details</h3>
|
|
||||||
|
|
||||||
<div class="mb-5">
|
|
||||||
<label class="block text-gray-700 text-sm font-bold mb-2 text-center" for="username">
|
|
||||||
Change username
|
|
||||||
</label>
|
|
||||||
<input class="input w-full shadow text-center" id="username" type="text" placeholder="Username" name="username"
|
|
||||||
[(ngModel)]="username"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="mb-5">
|
|
||||||
<label class="block text-gray-700 text-sm font-bold mb-2 text-center" for="password">
|
|
||||||
Current password
|
|
||||||
</label>
|
|
||||||
<input class="input w-full shadow text-center" id="password" type="password" placeholder="********" name="originalPassword"
|
|
||||||
[(ngModel)]="originalPassword">
|
|
||||||
</div>
|
|
||||||
<div class="mb-5">
|
|
||||||
<label class="block text-gray-700 text-sm font-bold mb-2 text-center" for="password2">
|
|
||||||
New password
|
|
||||||
</label>
|
|
||||||
<input class="input w-full shadow text-center" id="password2" type="password" placeholder="********" name="newPassword"
|
|
||||||
[(ngModel)]="newPassword">
|
|
||||||
</div>
|
|
||||||
<div class="mb-5">
|
|
||||||
<label class="block text-gray-700 text-sm font-bold mb-2 text-center" for="password3">
|
|
||||||
Confirm new password
|
|
||||||
</label>
|
|
||||||
<input class="input w-full shadow text-center" id="password3" type="password" placeholder="********" name="newPasswordConfirm"
|
|
||||||
[(ngModel)]="confirmNewPassword">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-action">
|
|
||||||
<form method="dialog">
|
|
||||||
<!-- if there is a button in form, it will close the modal -->
|
|
||||||
<button class="btn" (click)="saveUser()">Save</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { EdituserComponent } from './edituser.component';
|
|
||||||
|
|
||||||
describe('EdituserComponent', () => {
|
|
||||||
let component: EdituserComponent;
|
|
||||||
let fixture: ComponentFixture<EdituserComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [EdituserComponent]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(EdituserComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,63 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
<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>
|
|
@ -1,23 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,59 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,8 +4,6 @@ import {UploadComponent} from "./upload/upload.component";
|
|||||||
import {DownloadComponent} from "./download/download.component";
|
import {DownloadComponent} from "./download/download.component";
|
||||||
import {CreditsComponent} from "./credits/credits.component";
|
import {CreditsComponent} from "./credits/credits.component";
|
||||||
import {LicensesComponent} from "./credits/licenses/licenses.component";
|
import {LicensesComponent} from "./credits/licenses/licenses.component";
|
||||||
import {LoginComponent} from "./login/login.component";
|
|
||||||
import {AdminuiComponent} from "./adminui/adminui.component";
|
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{ path: '', redirectTo: 'home', pathMatch: 'full' },
|
{ path: '', redirectTo: 'home', pathMatch: 'full' },
|
||||||
@ -14,8 +12,6 @@ export const routes: Routes = [
|
|||||||
{ path: 'download', component: DownloadComponent },
|
{ path: 'download', component: DownloadComponent },
|
||||||
{ path: 'credits', component: CreditsComponent },
|
{ path: 'credits', component: CreditsComponent },
|
||||||
{ path: 'licenses', component: LicensesComponent },
|
{ path: 'licenses', component: LicensesComponent },
|
||||||
{ path: 'login', component: LoginComponent },
|
|
||||||
{ path: 'secure/administration', component: AdminuiComponent},
|
|
||||||
// { path: 'download/:id', component: DownloadComponent }
|
// { path: 'download/:id', component: DownloadComponent }
|
||||||
{ path: '**', redirectTo: 'home' }
|
{ path: '**', redirectTo: 'home' }
|
||||||
];
|
];
|
||||||
|
@ -42,15 +42,9 @@
|
|||||||
</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>
|
||||||
|
@ -32,7 +32,7 @@ export class CreditsComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getVersion() {
|
getVersion() {
|
||||||
axios.get(this.developmentStore.getBaseUrl() + 'api/v1/public/version')
|
axios.get(this.developmentStore.getBaseUrl() + 'api/v1/version')
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
this.version = response.data;
|
this.version = response.data;
|
||||||
})
|
})
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<img class="w-56 mt-6 mb-6" src="./assets/cloud-arrow-down-solid.svg">
|
<img class="w-56 mt-6 mb-6" src="./assets/cloud-arrow-down-solid.svg">
|
||||||
<input type="text" class="input input-bordered text-center w-full max-w-md mb-6" placeholder="Enter download code/link"
|
<input type="text" class="input input-bordered text-center w-full max-w-md mb-6" placeholder="Enter download code/link"
|
||||||
[(ngModel)]="inputFileId"
|
[(ngModel)]="inputFileId"
|
||||||
(keydown.enter)="requestDownload()"/>
|
(keydown.enter)="requestDownload()"/>
|
||||||
|
|
||||||
<div class="mb-6 w-full flex flex-col items-center justify-center">
|
<div class="mb-6 w-full flex flex-col items-center justify-center">
|
||||||
<input
|
<input
|
||||||
|
@ -99,7 +99,7 @@ export class DownloadComponent {
|
|||||||
|
|
||||||
axios({
|
axios({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
url: this.developmentStore.getBaseUrl() + 'api/v1/public/speed-test',
|
url: this.developmentStore.getBaseUrl() + 'api/v1/speed-test',
|
||||||
responseType: 'arraybuffer',
|
responseType: 'arraybuffer',
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
@ -119,7 +119,7 @@ export class DownloadComponent {
|
|||||||
private getDownloadInfo() {
|
private getDownloadInfo() {
|
||||||
axios({
|
axios({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
url: this.developmentStore.getBaseUrl() + 'api/v1/public/download-info?fileId=' + this.fileId,
|
url: this.developmentStore.getBaseUrl() + 'api/v1/download-info?fileId=' + this.fileId,
|
||||||
responseType: 'json',
|
responseType: 'json',
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
@ -138,7 +138,7 @@ export class DownloadComponent {
|
|||||||
this.fileDownloadStarted = true;
|
this.fileDownloadStarted = true;
|
||||||
axios({
|
axios({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
url: this.developmentStore.getBaseUrl() + 'api/v1/public/download?fileId=' + this.fileId + '&password=' + this.filePassword,
|
url: this.developmentStore.getBaseUrl() + 'api/v1/download?fileId=' + this.fileId + '&password=' + this.filePassword,
|
||||||
responseType: 'arraybuffer',
|
responseType: 'arraybuffer',
|
||||||
onDownloadProgress: (progressEvent) => {
|
onDownloadProgress: (progressEvent) => {
|
||||||
const endTime = new Date().getTime();
|
const endTime = new Date().getTime();
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
import { DurationPipe } from './duration.pipe';
|
|
||||||
|
|
||||||
describe('DurationPipe', () => {
|
|
||||||
it('create an instance', () => {
|
|
||||||
const pipe = new DurationPipe();
|
|
||||||
expect(pipe).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,29 +0,0 @@
|
|||||||
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`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
<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">
|
|
||||||
<h2 class="text-3xl font-bold text-gray-800 mb-10 text-center">Login to SharePulse</h2>
|
|
||||||
<form class="mb-10">
|
|
||||||
<div class="mb-5">
|
|
||||||
<label class="block text-gray-700 text-sm font-bold mb-2 text-center" for="username">
|
|
||||||
Username
|
|
||||||
</label>
|
|
||||||
<input class="input w-full shadow text-center" id="username" type="text" placeholder="Username"
|
|
||||||
[(ngModel)]="inputUsername" name="username"
|
|
||||||
[ngClass]="{'input-error': loginFailed}" autofocus >
|
|
||||||
</div>
|
|
||||||
<div class="mb-5">
|
|
||||||
<label class="block text-gray-700 text-sm font-bold mb-2 text-center" for="password">
|
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
<input class="input w-full shadow text-center" id="password" type="password" placeholder="********"
|
|
||||||
[(ngModel)]="inputPassword" name="password"
|
|
||||||
[ngClass]="{'input-error': loginFailed}"
|
|
||||||
(keydown.enter)="tryToLogin()">
|
|
||||||
</div>
|
|
||||||
<div class="mb-10">
|
|
||||||
<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"/>
|
|
||||||
<span class="ml-2 text-gray-700">Keep me signed in</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between mb-5">
|
|
||||||
<button *ngIf="!loginSuccessful" class="btn btn-primary w-full hover:scale-105 transition-transform duration-100" type="button" (click)="tryToLogin()">
|
|
||||||
Login
|
|
||||||
</button>
|
|
||||||
<button *ngIf="loginSuccessful" class="btn btn-success w-full hover:scale-105 transition-transform duration-100" type="button">
|
|
||||||
<span class="loading loading-ring loading-xs bg-white"></span>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="white" class="bi bi-check-lg" viewBox="0 0 16 16">
|
|
||||||
<path d="M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425z"/>
|
|
||||||
</svg>
|
|
||||||
<span class="loading loading-ring loading-xs bg-white"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<p class="text-gray-600 text-center">
|
|
||||||
Login to SharePulse to access the administrative dashboard and manage uploaded files. Please note that registration is disabled.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { LoginComponent } from './login.component';
|
|
||||||
|
|
||||||
describe('LoginComponent', () => {
|
|
||||||
let component: LoginComponent;
|
|
||||||
let fixture: ComponentFixture<LoginComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [LoginComponent]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(LoginComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,62 +0,0 @@
|
|||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
import { RelativeTimePipe } from './relative-time.pipe';
|
|
||||||
|
|
||||||
describe('RelativeTimePipe', () => {
|
|
||||||
it('create an instance', () => {
|
|
||||||
const pipe = new RelativeTimePipe();
|
|
||||||
expect(pipe).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,45 +0,0 @@
|
|||||||
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`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -97,7 +97,7 @@ export class UploadComponent {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
axios.post(this.developmentStore.getBaseUrl() + 'api/v1/public/upload', formData, config)
|
axios.post(this.developmentStore.getBaseUrl() + 'api/v1/upload', formData, config)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
console.log('Upload completed successfully!');
|
console.log('Upload completed successfully!');
|
||||||
console.log(response.data);
|
console.log(response.data);
|
||||||
@ -119,7 +119,7 @@ export class UploadComponent {
|
|||||||
passwordUrlPart = `&password=${fileDetails.password}`;
|
passwordUrlPart = `&password=${fileDetails.password}`;
|
||||||
}
|
}
|
||||||
const downloadUrl = `${baseUrl}download?fileId=${fileId}${passwordUrlPart}`;
|
const downloadUrl = `${baseUrl}download?fileId=${fileId}${passwordUrlPart}`;
|
||||||
const deleteUrl = `${baseUrl}api/v1/public/deletefile?fileId=${fileId}${passwordUrlPart}`;
|
const deleteUrl = `${baseUrl}api/v1/deletefile?fileId=${fileId}${passwordUrlPart}`;
|
||||||
const statusUrl = `${baseUrl}status?fileId=${fileId}${passwordUrlPart}`;
|
const statusUrl = `${baseUrl}status?fileId=${fileId}${passwordUrlPart}`;
|
||||||
return {
|
return {
|
||||||
downloadUrl,
|
downloadUrl,
|
||||||
@ -151,7 +151,7 @@ export class UploadComponent {
|
|||||||
|
|
||||||
axios({
|
axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: this.developmentStore.getBaseUrl() + 'api/v1/public/upload-speed-test',
|
url: this.developmentStore.getBaseUrl() + 'api/v1/upload-speed-test',
|
||||||
data: uint8View,
|
data: uint8View,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/octet-stream',
|
'Content-Type': 'application/octet-stream',
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
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<AuthStoreState> = new BehaviorSubject<AuthStoreState>({
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,15 +3,10 @@ package de.w665.sharepulse;
|
|||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class SharepulseApplication {
|
public class SharepulseApplication {
|
||||||
|
|
||||||
public static Date startTime;
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
startTime = new Date();
|
|
||||||
SpringApplication.run(SharepulseApplication.class, args);
|
SpringApplication.run(SharepulseApplication.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,6 @@ public class CorsConfig implements WebMvcConfigurer {
|
|||||||
registry.addMapping("/api/v1/**")
|
registry.addMapping("/api/v1/**")
|
||||||
.allowedOrigins("*")
|
.allowedOrigins("*")
|
||||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||||
.allowedHeaders("*")
|
.allowedHeaders("*");
|
||||||
.maxAge(3600);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -16,8 +16,6 @@ public class MvcConfig implements WebMvcConfigurer {
|
|||||||
registry.addViewController("/upload").setViewName("forward:/index.html");
|
registry.addViewController("/upload").setViewName("forward:/index.html");
|
||||||
registry.addViewController("/credits").setViewName("forward:/index.html");
|
registry.addViewController("/credits").setViewName("forward:/index.html");
|
||||||
registry.addViewController("/licenses").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");
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,54 +0,0 @@
|
|||||||
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
|
|
@ -3,44 +3,33 @@ package de.w665.sharepulse.db;
|
|||||||
import com.rethinkdb.RethinkDB;
|
import com.rethinkdb.RethinkDB;
|
||||||
import com.rethinkdb.gen.exc.ReqlOpFailedError;
|
import com.rethinkdb.gen.exc.ReqlOpFailedError;
|
||||||
import com.rethinkdb.net.Connection;
|
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.PostConstruct;
|
||||||
import jakarta.annotation.PreDestroy;
|
import jakarta.annotation.PreDestroy;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class RethinkDBService {
|
public class RethinkDBService {
|
||||||
|
|
||||||
private final RethinkDBConfig config;
|
private final RethinkDBConfig config;
|
||||||
|
|
||||||
private final RethinkDB r;
|
private final RethinkDB r;
|
||||||
private final Connection connection;
|
private final Connection connection;
|
||||||
private final UserRepository userRepository;
|
|
||||||
|
|
||||||
@Value("${sharepulse.auto-reset-on-startup}")
|
@Value("${sharepulse.auto-reset-on-startup}")
|
||||||
private boolean autoResetOnStartup;
|
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
|
@Autowired
|
||||||
public RethinkDBService(RethinkDBConfig config, RethinkDBConnector connector, UserRepository userRepository) {
|
public RethinkDBService(RethinkDBConfig config, RethinkDBConnector connector) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
|
||||||
|
|
||||||
// mapping to private vars for easier access
|
// mapping to private vars for easier access
|
||||||
this.r = connector.getR();
|
this.r = connector.getR();
|
||||||
this.connection = connector.getConnection();
|
this.connection = connector.getConnection();
|
||||||
this.userRepository = userRepository;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
@ -92,59 +81,9 @@ public class RethinkDBService {
|
|||||||
log.debug("Table 'expired_file_uploads' cleared successfully.");
|
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!");
|
log.info("Database ready for operation!");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeAdminUser() {
|
|
||||||
Optional<User> 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
|
@PreDestroy
|
||||||
public void close() {
|
public void close() {
|
||||||
if (connection != null) {
|
if (connection != null) {
|
||||||
|
@ -11,7 +11,6 @@ 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.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
@ -39,17 +38,4 @@ public class ExpiredFileUploadRepository {
|
|||||||
|
|
||||||
r.db("sharepulse").table("expired_file_uploads").insert(map).run(connection);
|
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<FileUpload> findAll() {
|
|
||||||
return r.db("sharepulse").table("expired_file_uploads")
|
|
||||||
.run(connection, FileUpload.class)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,6 @@ 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
|
||||||
@ -38,7 +36,7 @@ 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();
|
long uploadDateTimestamp = fileUpload.getUploadDate().getTime() / 1000;
|
||||||
map.put("uploadDate", uploadDateTimestamp);
|
map.put("uploadDate", uploadDateTimestamp);
|
||||||
|
|
||||||
r.db("sharepulse").table("file_uploads").insert(map).run(connection);
|
r.db("sharepulse").table("file_uploads").insert(map).run(connection);
|
||||||
@ -54,12 +52,7 @@ 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) {
|
||||||
@ -68,7 +61,7 @@ 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 = updatedFileUpload.getUploadDate().getTime();
|
long uploadDateTimestamp = updatedFileUpload.getUploadDate().getTime() / 1000;
|
||||||
map.put("uploadDate", uploadDateTimestamp);
|
map.put("uploadDate", uploadDateTimestamp);
|
||||||
|
|
||||||
String fileId = updatedFileUpload.getFileId();
|
String fileId = updatedFileUpload.getFileId();
|
||||||
@ -87,23 +80,25 @@ public class FileUploadRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<FileUpload> getAllExpiredFileUploads() {
|
public List<FileUpload> getAllExpiredFileUploads() {
|
||||||
long timestamp = getTimestamp24HoursAgo();
|
|
||||||
return r.db("sharepulse").table("file_uploads")
|
long timestamp = getOneMinuteAgoTimestamp();
|
||||||
|
|
||||||
|
List<FileUpload> olderFiles = 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();
|
||||||
|
return olderFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long getTimestamp24HoursAgo() {
|
private long get24HoursAgoTimestamp() {
|
||||||
Instant now = Instant.now();
|
Calendar calendar = Calendar.getInstance();
|
||||||
Instant oneMinuteAgo = now.minus(24, ChronoUnit.HOURS);
|
calendar.add(Calendar.HOUR, -24);
|
||||||
return oneMinuteAgo.toEpochMilli();
|
return calendar.getTimeInMillis() / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For testing only
|
private long getOneMinuteAgoTimestamp() {
|
||||||
private long getTimestampOneMinuteAgo() {
|
Calendar calendar = Calendar.getInstance();
|
||||||
Instant now = Instant.now();
|
calendar.add(Calendar.MINUTE, -1); // Subtract 1 minute
|
||||||
Instant oneMinuteAgo = now.minus(1, ChronoUnit.MINUTES);
|
return calendar.getTimeInMillis() / 1000; // Convert milliseconds to seconds (Unix timestamp)
|
||||||
return oneMinuteAgo.toEpochMilli();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,67 +0,0 @@
|
|||||||
package de.w665.sharepulse.db.repo;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.rethinkdb.RethinkDB;
|
|
||||||
import com.rethinkdb.net.Connection;
|
|
||||||
import com.rethinkdb.net.Result;
|
|
||||||
import de.w665.sharepulse.db.RethinkDBConfig;
|
|
||||||
import de.w665.sharepulse.db.RethinkDBConnector;
|
|
||||||
import de.w665.sharepulse.model.UserLogin;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Repository
|
|
||||||
public class UserLoginRepository {
|
|
||||||
private final RethinkDB r;
|
|
||||||
private final Connection connection;
|
|
||||||
private final RethinkDBConfig config;
|
|
||||||
private final String TABLE_NAME = "user_logins";
|
|
||||||
|
|
||||||
private final Gson gson = new Gson();
|
|
||||||
private final ObjectMapper mapper = new ObjectMapper();
|
|
||||||
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public UserLoginRepository(RethinkDBConnector connector, RethinkDBConfig config) {
|
|
||||||
this.r = connector.getR();
|
|
||||||
this.connection = connector.getConnection();
|
|
||||||
this.config = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void insertUserLogin(UserLogin userLogin) {
|
|
||||||
String uuid = r.uuid().run(connection, String.class).first();
|
|
||||||
userLogin.setId(uuid);
|
|
||||||
r.db(config.getDatabase()).table(TABLE_NAME).insert(userLogin).run(connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
public UserLogin getLastLogin(String userId) {
|
|
||||||
// Get the second most recent login (the most recent is the current one)
|
|
||||||
Result<UserLogin> result = r.db(config.getDatabase()).table(TABLE_NAME)
|
|
||||||
.orderBy().optArg("index", r.desc("loginTime"))
|
|
||||||
.filter(r.hashMap("userId", userId))
|
|
||||||
.skip(1).limit(1)
|
|
||||||
.run(connection, UserLogin.class);
|
|
||||||
// Return the second most recent login if exists
|
|
||||||
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,55 +0,0 @@
|
|||||||
package de.w665.sharepulse.db.repo;
|
|
||||||
|
|
||||||
import com.rethinkdb.RethinkDB;
|
|
||||||
import com.rethinkdb.net.Connection;
|
|
||||||
import de.w665.sharepulse.db.RethinkDBConfig;
|
|
||||||
import de.w665.sharepulse.db.RethinkDBConnector;
|
|
||||||
import de.w665.sharepulse.model.User;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
public class UserRepository {
|
|
||||||
private final RethinkDB r;
|
|
||||||
private final Connection connection;
|
|
||||||
private final RethinkDBConfig config;
|
|
||||||
@Autowired
|
|
||||||
public UserRepository(RethinkDBConnector connector, RethinkDBConfig config) {
|
|
||||||
this.r = connector.getR();
|
|
||||||
this.connection = connector.getConnection();
|
|
||||||
this.config = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<User> retrieveUserByUsername(String username) {
|
|
||||||
try {
|
|
||||||
User user = r.db(config.getDatabase()).table("users")
|
|
||||||
.filter(r.hashMap("username", username))
|
|
||||||
.run(connection, User.class)
|
|
||||||
.next();
|
|
||||||
return Optional.ofNullable(user);
|
|
||||||
} catch (NoSuchElementException e) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateLastLoginForUser(String username, Date lastLogin) {
|
|
||||||
r.db(config.getDatabase()).table("users")
|
|
||||||
.filter(r.hashMap("username", username))
|
|
||||||
.update(r.hashMap("lastLogin", lastLogin.getTime()))
|
|
||||||
.run(connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateUser(User user) {
|
|
||||||
r.db(config.getDatabase()).table("users")
|
|
||||||
.filter(r.hashMap("id", user.getId()))
|
|
||||||
.update(user)
|
|
||||||
.run(connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void insertUser(User user) {
|
|
||||||
String optionalUuid = r.uuid().run(connection, String.class).first();
|
|
||||||
user.setId(optionalUuid);
|
|
||||||
r.db(config.getDatabase()).table("users").insert(user).run(connection);
|
|
||||||
}
|
|
||||||
}
|
|
@ -18,7 +18,6 @@ public class FileUpload {
|
|||||||
private String fileName;
|
private String fileName;
|
||||||
private long fileSize;
|
private long fileSize;
|
||||||
private boolean singleDownload;
|
private boolean singleDownload;
|
||||||
private boolean disabled;
|
|
||||||
@JsonFormat(timezone = "ETC")
|
@JsonFormat(timezone = "ETC")
|
||||||
private Date uploadDate;
|
private Date uploadDate;
|
||||||
private String uploadedByIpAddress;
|
private String uploadedByIpAddress;
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
package de.w665.sharepulse.model;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
|
||||||
import lombok.*;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@ToString
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class User {
|
|
||||||
private String id; // ID is auto mapped by RethinkDB
|
|
||||||
private String username;
|
|
||||||
private String password;
|
|
||||||
private String email;
|
|
||||||
private String role;
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
package de.w665.sharepulse.model;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class UserLogin {
|
|
||||||
String id;
|
|
||||||
String userId;
|
|
||||||
@JsonFormat(timezone = "ETC")
|
|
||||||
Date loginTime;
|
|
||||||
String loginIp;
|
|
||||||
}
|
|
@ -4,6 +4,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/public")
|
@RequestMapping("/api/v1")
|
||||||
public abstract class ApiRestController {
|
public abstract class ApiRestController {
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
package de.w665.sharepulse.rest;
|
package de.w665.sharepulse.rest;
|
||||||
|
|
||||||
import org.springframework.boot.web.servlet.error.ErrorController;
|
import org.springframework.boot.web.servlet.error.ErrorController;
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@ -12,19 +10,10 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
@RestController
|
@RestController
|
||||||
public class ErrorRestController implements ErrorController {
|
public class ErrorRestController implements ErrorController {
|
||||||
@RequestMapping("/error")
|
@RequestMapping("/error")
|
||||||
public ResponseEntity<Object> handleError() {
|
public String handleError() {
|
||||||
String script = "<script>" +
|
return "<script>" +
|
||||||
"console.log('Page not found. Redirecting to /home...');" +
|
"console.log('Page not found. Redirecting to /home...');" +
|
||||||
"window.location.href = window.location.origin + '/home';" +
|
"window.location.href = window.location.origin + '/home';" +
|
||||||
"</script>" +
|
"</script>";
|
||||||
"<h1>You've reached the default error page.</h1>" +
|
|
||||||
"<p>This could be caused by several reasons:</p>" +
|
|
||||||
"<ul>" +
|
|
||||||
"<li>The page you are looking for does not exist.</li>" +
|
|
||||||
"<li>The page you are looking for is not accessible.</li>" +
|
|
||||||
"<li>The page you are looking for is not available at the moment.</li>" +
|
|
||||||
"<li>The page you are looking for is not accessible without authentication.</li>" +
|
|
||||||
"</ul>";
|
|
||||||
return new ResponseEntity<>(script, HttpStatus.FOUND);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
package de.w665.sharepulse.rest;
|
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/v1/secure")
|
|
||||||
public abstract class SecureApiRestController {
|
|
||||||
}
|
|
@ -1,126 +0,0 @@
|
|||||||
package de.w665.sharepulse.rest.mappings;
|
|
||||||
|
|
||||||
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.UserRepository;
|
|
||||||
import de.w665.sharepulse.model.FileUpload;
|
|
||||||
import de.w665.sharepulse.model.User;
|
|
||||||
import de.w665.sharepulse.rest.SecureApiRestController;
|
|
||||||
import de.w665.sharepulse.rest.ro.FileDeleteRequest;
|
|
||||||
import de.w665.sharepulse.rest.ro.UserEditRequest;
|
|
||||||
import de.w665.sharepulse.service.AuthenticationService;
|
|
||||||
import de.w665.sharepulse.service.FileCleanupService;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@RestController
|
|
||||||
public class Administration extends SecureApiRestController {
|
|
||||||
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
private final AuthenticationService authenticationService;
|
|
||||||
private final FileCleanupService fileCleanupService;
|
|
||||||
private final UserLoginRepository userLoginRepository;
|
|
||||||
private final FileUploadRepository fileUploadRepository;
|
|
||||||
|
|
||||||
public Administration(UserRepository userRepository, AuthenticationService authenticationService, FileCleanupService fileCleanupService, UserLoginRepository userLoginRepository, FileUploadRepository fileUploadRepository) {
|
|
||||||
this.userRepository = userRepository;
|
|
||||||
this.authenticationService = authenticationService;
|
|
||||||
this.fileCleanupService = fileCleanupService;
|
|
||||||
this.userLoginRepository = userLoginRepository;
|
|
||||||
this.fileUploadRepository = fileUploadRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/statistics")
|
|
||||||
public ResponseEntity<Object> getStatistics(HttpServletRequest request) {
|
|
||||||
// TODO: FIX LAST LOGIN
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
User user = optionalUser.get();
|
|
||||||
|
|
||||||
// If role filtering is required later, add here
|
|
||||||
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
|
||||||
response.put("applicationOnlineTime", System.currentTimeMillis() - SharepulseApplication.startTime.getTime());
|
|
||||||
response.put("lastUserLogin", userLoginRepository.getLastLogin(user.getId()));
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/users")
|
|
||||||
public ResponseEntity<Object> updateUser(@RequestBody UserEditRequest userEditRequest, HttpServletRequest request) {
|
|
||||||
Optional<User> optionalUser = userRepository.retrieveUserByUsername(userEditRequest.getOriginalUsername());
|
|
||||||
if(optionalUser.isEmpty()) {
|
|
||||||
return ResponseEntity.badRequest().body("User not found");
|
|
||||||
} else if (!userEditRequest.getNewPassword().equals(userEditRequest.getNewPasswordConfirm())) {
|
|
||||||
return ResponseEntity.badRequest().body("Passwords do not match");
|
|
||||||
} else if(userEditRequest.getNewPassword().length() < 4) {
|
|
||||||
return ResponseEntity.badRequest().body("Password too short. Must be at least 4 characters.");
|
|
||||||
}
|
|
||||||
User user = optionalUser.get();
|
|
||||||
user.setPassword(authenticationService.encodePassword(userEditRequest.getNewPassword()));
|
|
||||||
user.setUsername(userEditRequest.getUsername());
|
|
||||||
userRepository.updateUser(user);
|
|
||||||
// Clear password before returning
|
|
||||||
user.setPassword(null);
|
|
||||||
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")
|
|
||||||
public ResponseEntity<Object> deleteFiles(HttpServletRequest request) {
|
|
||||||
List<FileUpload> files = fileCleanupService.deleteFiles();
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
package de.w665.sharepulse.rest.mappings;
|
|
||||||
|
|
||||||
import de.w665.sharepulse.rest.ro.AuthenticationRequest;
|
|
||||||
import de.w665.sharepulse.service.AuthenticationService;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/v1/auth")
|
|
||||||
public class AuthenticationController {
|
|
||||||
|
|
||||||
private final AuthenticationService authenticationService;
|
|
||||||
|
|
||||||
public AuthenticationController(AuthenticationService authenticationService) {
|
|
||||||
this.authenticationService = authenticationService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/login")
|
|
||||||
public ResponseEntity<Object> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest, HttpServletRequest request) {
|
|
||||||
log.debug("Received AuthenticationRequest for username: " + authenticationRequest.getUsername());
|
|
||||||
String token = authenticationService.authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword(), request.getRemoteAddr());
|
|
||||||
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
|
||||||
response.put("token", token);
|
|
||||||
response.put("success", token != null);
|
|
||||||
|
|
||||||
if(token == null) {
|
|
||||||
log.debug("Authentication failed for username: " + authenticationRequest.getUsername());
|
|
||||||
return new ResponseEntity<>(response, HttpStatus.UNAUTHORIZED);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ResponseEntity<>(response, HttpStatus.OK);
|
|
||||||
}
|
|
||||||
}
|
|
@ -35,7 +35,7 @@ public class Upload extends ApiRestController {
|
|||||||
@RequestParam(value = "singleDownload", defaultValue = "false") boolean singleDownload,
|
@RequestParam(value = "singleDownload", defaultValue = "false") boolean singleDownload,
|
||||||
@RequestParam(value = "fileDescription", required = false) String fileDescription) {
|
@RequestParam(value = "fileDescription", required = false) String fileDescription) {
|
||||||
|
|
||||||
// TODO: Handle shortStorage
|
// TODO: Handle shortStorage0
|
||||||
|
|
||||||
if (file.isEmpty()) {
|
if (file.isEmpty()) {
|
||||||
log.debug("User tried to upload an empty file. IP: " + request.getRemoteAddr());
|
log.debug("User tried to upload an empty file. IP: " + request.getRemoteAddr());
|
||||||
@ -53,7 +53,7 @@ public class Upload extends ApiRestController {
|
|||||||
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
|
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("User uploaded file \"" + file.getOriginalFilename() + "\" from IP " + request.getRemoteAddr() + " successfully.");
|
log.debug("User uploaded file " + file.getOriginalFilename() + " from IP " + request.getRemoteAddr() + " successfully.");
|
||||||
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
Map<String, Object> response = new HashMap<>();
|
||||||
response.put("fileId", fileUpload.getFileId());
|
response.put("fileId", fileUpload.getFileId());
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
package de.w665.sharepulse.rest.mappings;
|
|
||||||
|
|
||||||
import de.w665.sharepulse.db.repo.ExpiredFileUploadRepository;
|
|
||||||
import de.w665.sharepulse.db.repo.FileUploadRepository;
|
|
||||||
import de.w665.sharepulse.model.FileUpload;
|
|
||||||
import de.w665.sharepulse.rest.SecureApiRestController;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@RestController
|
|
||||||
public class UploadHistory extends SecureApiRestController {
|
|
||||||
|
|
||||||
private final FileUploadRepository fileUploadRepository;
|
|
||||||
private final ExpiredFileUploadRepository expiredFileUploadRepository;
|
|
||||||
|
|
||||||
public UploadHistory(FileUploadRepository fileUploadRepository, ExpiredFileUploadRepository expiredFileUploadRepository) {
|
|
||||||
this.fileUploadRepository = fileUploadRepository;
|
|
||||||
this.expiredFileUploadRepository = expiredFileUploadRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/test")
|
|
||||||
public ResponseEntity<Object> test(HttpServletRequest request) {
|
|
||||||
log.debug("Received test request");
|
|
||||||
return ResponseEntity.ok("Test successful");
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/upload-history")
|
|
||||||
public ResponseEntity<Object> getUploadHistory(HttpServletRequest request, HttpServletResponse response) {
|
|
||||||
List<FileUpload> fileUploadList = fileUploadRepository.findAll();
|
|
||||||
log.debug("Received upload history request");
|
|
||||||
return ResponseEntity.ok(fileUploadList);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/expired-upload-history")
|
|
||||||
public ResponseEntity<Object> getExpiredUploadHistory(HttpServletRequest request, HttpServletResponse response) {
|
|
||||||
List<FileUpload> expiredFileUploadList = expiredFileUploadRepository.findAll();
|
|
||||||
log.debug("Received expired upload history request");
|
|
||||||
return ResponseEntity.ok(expiredFileUploadList);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package de.w665.sharepulse.rest.ro;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.ToString;
|
|
||||||
|
|
||||||
@NoArgsConstructor
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@ToString
|
|
||||||
public class AuthenticationRequest {
|
|
||||||
private String username;
|
|
||||||
private String password;
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
package de.w665.sharepulse.rest.ro;
|
|
||||||
|
|
||||||
import lombok.*;
|
|
||||||
|
|
||||||
@NoArgsConstructor
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@ToString
|
|
||||||
public class UserEditRequest {
|
|
||||||
private String originalUsername;
|
|
||||||
private String username;
|
|
||||||
private String originalPassword;
|
|
||||||
private String newPassword;
|
|
||||||
private String newPasswordConfirm;
|
|
||||||
private String email; // E-Mail is not implemented into frontend yet
|
|
||||||
}
|
|
@ -1,74 +0,0 @@
|
|||||||
package de.w665.sharepulse.rest.security;
|
|
||||||
|
|
||||||
import de.w665.sharepulse.service.AuthenticationService;
|
|
||||||
import jakarta.servlet.FilterChain;
|
|
||||||
import jakarta.servlet.ServletException;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
|
||||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
|
||||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
@Slf4j
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|
||||||
|
|
||||||
private final AuthenticationService authenticationService;
|
|
||||||
private final RequestMatcher requestMatcher = new AntPathRequestMatcher("/api/v1/secure/**");
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
|
|
||||||
|
|
||||||
logger.debug("Filtering request: " + request.getRequestURI());
|
|
||||||
|
|
||||||
if(!requestMatcher.matches(request)) {
|
|
||||||
logger.debug("Request does not match the secure path. Skipping JWT authentication.");
|
|
||||||
filterChain.doFilter(request, response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
String jwt = getJwtFromRequest(request);
|
|
||||||
if (jwt != null && authenticationService.validateToken(jwt)) {
|
|
||||||
String username = authenticationService.extractSubject(jwt);
|
|
||||||
// Extract the role from the JWT and set it to Spring AuthenticationContext for access control
|
|
||||||
String role = authenticationService.getClaimValue(jwt, "role", String.class);
|
|
||||||
List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role));
|
|
||||||
|
|
||||||
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, authorities);
|
|
||||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
|
||||||
// SUCCESSFUL AUTHENTICATION
|
|
||||||
filterChain.doFilter(request, response);
|
|
||||||
} else {
|
|
||||||
logger.warn("Unauthorized: Authentication token is missing or invalid.");
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
logger.warn("Could not set user authentication in security context. An error occurred during JWT processing.", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getJwtFromRequest(HttpServletRequest request) {
|
|
||||||
String bearerToken = request.getHeader("Authorization");
|
|
||||||
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
|
|
||||||
return bearerToken.substring(7);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,112 +0,0 @@
|
|||||||
package de.w665.sharepulse.service;
|
|
||||||
|
|
||||||
import de.w665.sharepulse.db.repo.UserLoginRepository;
|
|
||||||
import de.w665.sharepulse.db.repo.UserRepository;
|
|
||||||
import de.w665.sharepulse.model.User;
|
|
||||||
import de.w665.sharepulse.model.UserLogin;
|
|
||||||
import io.jsonwebtoken.*;
|
|
||||||
import io.jsonwebtoken.security.Keys;
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
public class AuthenticationService {
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
private final UserLoginRepository userLoginRepository;
|
|
||||||
|
|
||||||
@Value("${secureapi.jwt.secret}")
|
|
||||||
private String secretString;
|
|
||||||
@Value("${secureapi.jwt.expiration}")
|
|
||||||
private long expirationTime; // in milliseconds
|
|
||||||
private SecretKey secretKey;
|
|
||||||
|
|
||||||
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
|
||||||
|
|
||||||
public AuthenticationService(UserRepository userRepository, UserLoginRepository userLoginRepository) {
|
|
||||||
this.userRepository = userRepository;
|
|
||||||
this.userLoginRepository = userLoginRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
public void init() {
|
|
||||||
log.debug("Initializing secret key");
|
|
||||||
byte[] encodedKey = Base64.getEncoder().encode(secretString.getBytes()); // encode the secret key
|
|
||||||
this.secretKey = Keys.hmacShaKeyFor(encodedKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String authenticate(String username, String password, String remoteAddr, long... expirationTime/*FOR TESTING VALIDITY*/) {
|
|
||||||
if(expirationTime.length > 0) {
|
|
||||||
this.expirationTime = expirationTime[0];
|
|
||||||
}
|
|
||||||
Optional<User> optionalUser = userRepository.retrieveUserByUsername(username);
|
|
||||||
if (optionalUser.isPresent() && passwordEncoder.matches(password, optionalUser.get().getPassword())) {
|
|
||||||
User user = optionalUser.get();
|
|
||||||
|
|
||||||
userLoginRepository.insertUserLogin(new UserLogin(""/*Auto generated*/, user.getId(), new Date(), remoteAddr));
|
|
||||||
userRepository.updateLastLoginForUser(user.getUsername(), new Date());
|
|
||||||
return generateToken(user);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String generateToken(User username) {
|
|
||||||
long nowMillis = System.currentTimeMillis();
|
|
||||||
Date now = new Date(nowMillis);
|
|
||||||
Date expiryDate = new Date(nowMillis + expirationTime);
|
|
||||||
|
|
||||||
return Jwts.builder()
|
|
||||||
.subject("SharePulse Authentication Token")
|
|
||||||
.issuedAt(now)
|
|
||||||
.claim("role", username.getRole())
|
|
||||||
.claim("username", username.getUsername())
|
|
||||||
.expiration(expiryDate)
|
|
||||||
.signWith(secretKey)
|
|
||||||
.compact();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean validateToken(String token) {
|
|
||||||
try {
|
|
||||||
Jwt<?,?> jwt = Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token);
|
|
||||||
Claims claims = (Claims) jwt.getPayload();
|
|
||||||
return !claims.getExpiration().before(new Date()); // Checks if the token is expired too
|
|
||||||
} catch (Exception e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String extractSubject(String token) {
|
|
||||||
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getSubject();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves a typed claim from the JWT.
|
|
||||||
* @param token the JWT from which to extract the claim
|
|
||||||
* @param claimName the name of the claim to retrieve
|
|
||||||
* @param claimType the Class object of <T> the expected type of the claim value
|
|
||||||
* @return the value of the specified claim as type T, or null if not found or in case of an error
|
|
||||||
* Usage example: getClaimValue(token, "role", String.class)
|
|
||||||
*/
|
|
||||||
public <T> T getClaimValue(String token, String claimName, Class<T> claimType) {
|
|
||||||
try {
|
|
||||||
Jwt<?, ?> jwt = Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token);
|
|
||||||
Claims claims = (Claims) jwt.getPayload();
|
|
||||||
return claims.get(claimName, claimType);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error parsing claims from token: ", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String encodePassword(String password) {
|
|
||||||
return passwordEncoder.encode(password);
|
|
||||||
}
|
|
||||||
}
|
|
@ -23,7 +23,6 @@ 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...");
|
||||||
@ -34,15 +33,4 @@ public class FileCleanupService {
|
|||||||
log.debug("Moved file " + fileUpload.getFileId() + " to old_file_uploads table.");
|
log.debug("Moved file " + fileUpload.getFileId() + " to old_file_uploads table.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FileUpload> deleteFiles() {
|
|
||||||
log.debug("Running cleanup. Clearing all files...");
|
|
||||||
List<FileUpload> fileUploads = fileUploadRepository.findAll();
|
|
||||||
for (FileUpload fileUpload : fileUploads) {
|
|
||||||
fileService.deleteFile(fileUpload);
|
|
||||||
expiredFileUploadRepository.insertExpiredFileUpload(fileUpload);
|
|
||||||
log.debug("Moved file " + fileUpload.getFileId() + " to old_file_uploads table.");
|
|
||||||
}
|
|
||||||
return fileUploads;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -21,9 +21,6 @@ public class FileSecurityService {
|
|||||||
|
|
||||||
public boolean verifyDownloadPermission(FileUpload file, String password) throws NoDownloadPermissionException {
|
public boolean verifyDownloadPermission(FileUpload file, String password) throws NoDownloadPermissionException {
|
||||||
|
|
||||||
if(file.isDisabled()) {
|
|
||||||
throw new NoDownloadPermissionException("File download has been disabled.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!file.isPasswordProtected() && !file.isSingleDownload()) {
|
if (!file.isPasswordProtected() && !file.isSingleDownload()) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -85,7 +85,6 @@ public class FileService {
|
|||||||
.fileName(file.getOriginalFilename())
|
.fileName(file.getOriginalFilename())
|
||||||
.fileSize(file.getSize())
|
.fileSize(file.getSize())
|
||||||
.singleDownload(singleDownload)
|
.singleDownload(singleDownload)
|
||||||
.disabled(false)
|
|
||||||
.uploadDate(new Date())
|
.uploadDate(new Date())
|
||||||
.uploadedByIpAddress(uploaderIp)
|
.uploadedByIpAddress(uploaderIp)
|
||||||
.downloadCount(0)
|
.downloadCount(0)
|
||||||
|
@ -5,9 +5,6 @@ sharepulse.fileid.length=6
|
|||||||
sharepulse.fileid.charset=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
|
sharepulse.fileid.charset=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
|
||||||
sharepulse.filepassword.length=6
|
sharepulse.filepassword.length=6
|
||||||
sharepulse.filepassword.charset=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
|
sharepulse.filepassword.charset=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
|
||||||
sharepulse.management.user.username=admin
|
|
||||||
sharepulse.management.user.password=admin
|
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
rethinkdb.host=localhost
|
rethinkdb.host=localhost
|
||||||
@ -28,6 +25,4 @@ spring.web.resources.static-locations=classpath:/static/browser/
|
|||||||
spring.data.rest.base-path=/api/v1
|
spring.data.rest.base-path=/api/v1
|
||||||
# Miscellaneous
|
# Miscellaneous
|
||||||
server.port=80
|
server.port=80
|
||||||
spring.application.name=sharepulse
|
spring.application.name=sharepulse
|
||||||
secureapi.jwt.secret=sampleKeyToChangeInProduction
|
|
||||||
secureapi.jwt.expiration=3600000
|
|
@ -1,65 +0,0 @@
|
|||||||
package de.w665.sharepulse;
|
|
||||||
|
|
||||||
import de.w665.sharepulse.db.repo.UserRepository;
|
|
||||||
import de.w665.sharepulse.model.User;
|
|
||||||
import de.w665.sharepulse.service.AuthenticationService;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
@SpringBootTest
|
|
||||||
public class AuthenticationServiceTest {
|
|
||||||
|
|
||||||
@MockBean
|
|
||||||
private UserRepository userRepository;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private AuthenticationService authenticationService;
|
|
||||||
|
|
||||||
private final String username = "testUser";
|
|
||||||
private final String password = "testPass";
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
public void setup() {
|
|
||||||
User mockUser = new User();
|
|
||||||
mockUser.setUsername(username);
|
|
||||||
mockUser.setPassword(new BCryptPasswordEncoder().encode(password));
|
|
||||||
when(userRepository.retrieveUserByUsername(anyString())).thenReturn(Optional.of(mockUser));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void whenValidUsernameAndPassword_thenAuthenticateShouldReturnToken() {
|
|
||||||
String token = authenticationService.authenticate(username, password, "");
|
|
||||||
assertNotNull(token, "Token should not be null for valid credentials");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void whenValidToken_thenValidateTokenShouldReturnTrue() {
|
|
||||||
String token = authenticationService.authenticate(username, password, "");
|
|
||||||
assertTrue(authenticationService.validateToken(token), "Token validation should return true for a valid token");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void whenInvalidToken_thenValidateTokenShouldReturnFalse() {
|
|
||||||
String invalidToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0VXNlciJ9.WrongSignature";
|
|
||||||
assertFalse(authenticationService.validateToken(invalidToken), "Token validation should return false for an invalid token");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void whenTokenIsExplicitlyExpired_thenValidateTokenShouldReturnFalse() throws InterruptedException {
|
|
||||||
long testExpirationTime = 1; // 1 millisecond
|
|
||||||
String token = authenticationService.authenticate("testUser", "testPass", "", testExpirationTime);
|
|
||||||
assertNotNull(token, "Token should not be null");
|
|
||||||
Thread.sleep(2); // Wait for 2 milliseconds to ensure the token has expired (Bad practice but easy)
|
|
||||||
assertFalse(authenticationService.validateToken(token), "Expired token should not be valid");
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user