0.1 #1
114 changed files with 8476 additions and 7698 deletions
54
.drone.yml
54
.drone.yml
|
@ -19,6 +19,46 @@ steps:
|
||||||
- cd service/fling
|
- cd service/fling
|
||||||
- mvn -Pprod clean deploy
|
- mvn -Pprod clean deploy
|
||||||
|
|
||||||
|
- name: runservice
|
||||||
|
image: adoptopenjdk:11-jre-hotspot
|
||||||
|
commands:
|
||||||
|
- cd service/fling/target
|
||||||
|
- java -jar fling-0.1.0-SNAPSHOT.jar
|
||||||
|
detach: true
|
||||||
|
|
||||||
|
- name: generate-clients
|
||||||
|
image: alpine
|
||||||
|
environment:
|
||||||
|
NEXUS_USER:
|
||||||
|
from_secret: nexus_user
|
||||||
|
NEXUS_PASSWORD:
|
||||||
|
from_secret: nexus_password
|
||||||
|
commands:
|
||||||
|
- apk add --update --no-cache openjdk11 npm
|
||||||
|
- sleep 20
|
||||||
|
- npm install @openapitools/openapi-generator-cli -g
|
||||||
|
# Python client
|
||||||
|
- openapi-generator generate
|
||||||
|
-i http://runservice:8080/v3/api-docs
|
||||||
|
-g python
|
||||||
|
--additional-properties packageName=flingclient
|
||||||
|
-o flingclient.py
|
||||||
|
--enable-post-process-file
|
||||||
|
- cd flingclient.py
|
||||||
|
- cd ..
|
||||||
|
# JavaScript client
|
||||||
|
- openapi-generator generate
|
||||||
|
-i http://runservice:8080/v3/api-docs
|
||||||
|
-g javascript
|
||||||
|
--additional-properties projectName=@fling/flingclient,usePromises=true,npmRepository=https://nexus.friedl.net/repository/npm-private/
|
||||||
|
-o flingclient.js
|
||||||
|
--enable-post-process-file
|
||||||
|
- cd flingclient.js && npm install && npm run build
|
||||||
|
- echo "https://nexus.friedl.net/repository/npm-private/" >> .npmrc
|
||||||
|
- echo -n "_auth=" >> .npmrc && echo -n "$NEXUS_USER:$NEXUS_PASSWORD" | base64 >> .npmrc
|
||||||
|
- echo "email=dev@friedl.net" >> .npmrc
|
||||||
|
- npm publish
|
||||||
|
|
||||||
- name: build-web
|
- name: build-web
|
||||||
image: node:latest
|
image: node:latest
|
||||||
volumes:
|
volumes:
|
||||||
|
@ -29,12 +69,15 @@ steps:
|
||||||
from_secret: nexus_user
|
from_secret: nexus_user
|
||||||
NEXUS_PASSWORD:
|
NEXUS_PASSWORD:
|
||||||
from_secret: nexus_password
|
from_secret: nexus_password
|
||||||
|
VERSION: 0.1.0-snapshot
|
||||||
commands:
|
commands:
|
||||||
- ls -al
|
- ls -al
|
||||||
- cd web/fling
|
- cd web/fling
|
||||||
- npm install && npm run build
|
- npm install && npm run build
|
||||||
- tar czf fling-web-latest.tar.gz build/
|
- tar czf fling-web-$VERSION.tar.gz build/
|
||||||
- curl --user "$NEXUS_USER:$NEXUS_PASSWORD" --upload-file ./fling-web-latest.tar.gz https://nexus.friedl.net/repository/build-artifacts/fling-web-latest.tar.gz
|
- curl --user "$NEXUS_USER:$NEXUS_PASSWORD"
|
||||||
|
--upload-file ./fling-web-$VERSION.tar.gz
|
||||||
|
https://nexus.friedl.net/repository/build-artifacts/fling-web-$VERSION.tar.gz
|
||||||
|
|
||||||
- name: publish
|
- name: publish
|
||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
|
@ -46,10 +89,9 @@ steps:
|
||||||
dockerfile: container/Dockerfile
|
dockerfile: container/Dockerfile
|
||||||
context: ./container
|
context: ./container
|
||||||
repo: arminfriedl/fling
|
repo: arminfriedl/fling
|
||||||
tags: latest
|
tags: 0.1.0-snapshot
|
||||||
when:
|
build_args:
|
||||||
branch:
|
- VERSION=0.1.0-snapshot
|
||||||
- master
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- name: m2-cache
|
- name: m2-cache
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -243,7 +243,6 @@ modules.xml
|
||||||
*.war
|
*.war
|
||||||
*.nar
|
*.nar
|
||||||
*.ear
|
*.ear
|
||||||
*.zip
|
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.rar
|
*.rar
|
||||||
|
|
||||||
|
@ -321,6 +320,7 @@ pids
|
||||||
*.pid
|
*.pid
|
||||||
*.seed
|
*.seed
|
||||||
*.pid.lock
|
*.pid.lock
|
||||||
|
.attach_pid*
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
lib-cov
|
lib-cov
|
||||||
|
|
9
LICENSE
Normal file
9
LICENSE
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright © 2020 Armin Friedl
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
1
VERSION
Normal file
1
VERSION
Normal file
|
@ -0,0 +1 @@
|
||||||
|
0.1.0-snapshot
|
|
@ -1,10 +1,12 @@
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
|
|
||||||
|
ARG VERSION
|
||||||
|
|
||||||
RUN apk add --update --no-cache nginx openjdk11-jre && \
|
RUN apk add --update --no-cache nginx openjdk11-jre && \
|
||||||
mkdir -p /var/fling/files && \
|
mkdir -p /var/fling/files && \
|
||||||
mkdir -p /tmp/fling && \
|
mkdir -p /tmp/fling && \
|
||||||
wget -O /tmp/fling/service.jar "https://nexus.friedl.net/service/rest/v1/search/assets/download?sort=version&maven.groupId=net.friedl&maven.artifactId=fling&maven.baseVersion=*SNAPSHOT&maven.extension=jar" && \
|
wget -O /tmp/fling/service.jar "https://nexus.friedl.net/service/rest/v1/search/assets/download?sort=version&maven.groupId=net.friedl&maven.artifactId=fling&maven.baseVersion=$(echo -n $VERSION | tr [:lower:] [:upper:])&maven.extension=jar" && \
|
||||||
wget -O /tmp/fling/web.tar.gz "https://nexus.friedl.net/repository/build-artifacts/fling-web-latest.tar.gz" && \
|
wget -O /tmp/fling/web.tar.gz "https://nexus.friedl.net/repository/build-artifacts/fling-web-$(echo -n $VERSION | tr [:upper:] [:lower:]).tar.gz" && \
|
||||||
tar xzf /tmp/fling/web.tar.gz -C /tmp/fling && \
|
tar xzf /tmp/fling/web.tar.gz -C /tmp/fling && \
|
||||||
ls -al /tmp/fling && \
|
ls -al /tmp/fling && \
|
||||||
mkdir -p /var/www/fling && mv /tmp/fling/build/* /var/www/fling && \
|
mkdir -p /var/www/fling && mv /tmp/fling/build/* /var/www/fling && \
|
||||||
|
|
|
@ -27,6 +27,40 @@ server {
|
||||||
client_max_body_size 5G;
|
client_max_body_size 5G;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# handle openapi requests by openapi servlet
|
||||||
|
location /v3/api-docs {
|
||||||
|
proxy_pass http://localhost:8080;
|
||||||
|
|
||||||
|
proxy_set_header X-Forwarded-Host $host:$server_port;
|
||||||
|
proxy_set_header X-Forwarded-Server $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# Required for web sockets to function
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
# handle openapi requests by openapi servlet
|
||||||
|
location /swagger-ui.html {
|
||||||
|
proxy_pass http://localhost:8080;
|
||||||
|
|
||||||
|
proxy_set_header X-Forwarded-Host $host:$server_port;
|
||||||
|
proxy_set_header X-Forwarded-Server $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# Required for web sockets to function
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
# always respond with index.html for unknown paths
|
# always respond with index.html for unknown paths
|
||||||
# (routing is client side)
|
# (routing is client side)
|
||||||
location / {
|
location / {
|
||||||
|
|
32
examples/python/fling.py
Normal file
32
examples/python/fling.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import flingclient as fc
|
||||||
|
from flingclient.rest import ApiException
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Per default the dockerized fling service runs on localhost:3000 In case you
|
||||||
|
# run your own instance, change the base url
|
||||||
|
configuration = fc.Configuration(host="http://localhost:3000")
|
||||||
|
|
||||||
|
# Every call, with the exception of `/api/auth`, is has to be authorized by a
|
||||||
|
# bearer token. Get a token by authenticating as admin and set it into the
|
||||||
|
# configuration. All subsequent calls will send this token in the header as
|
||||||
|
# `Authorization: Bearer <token> header`
|
||||||
|
def authenticate(admin_user, admin_password):
|
||||||
|
with fc.ApiClient(configuration) as api_client:
|
||||||
|
auth_client = fc.AuthApi(api_client)
|
||||||
|
admin_auth = fc.AdminAuth(admin_user, admin_password)
|
||||||
|
configuration.access_token = auth_client.authenticate_owner(admin_auth=admin_auth)
|
||||||
|
|
||||||
|
admin_user = input("Username: ")
|
||||||
|
admin_password = input("Password: ")
|
||||||
|
authenticate(admin_user, admin_password)
|
||||||
|
|
||||||
|
with fc.ApiClient(configuration) as api_client:
|
||||||
|
# Create a new fling
|
||||||
|
fling_client = fc.FlingApi(api_client)
|
||||||
|
fling = fc.Fling(name="A Fling from Python", auth_code="secret",
|
||||||
|
direct_download=False, allow_upload=True,
|
||||||
|
expiration_time=datetime(2099, 12, 12))
|
||||||
|
fling = fling_client.post_fling()
|
||||||
|
print(f"Created a new fling: {fling}")
|
||||||
|
|
||||||
|
#
|
98
examples/querysheet.http
Normal file
98
examples/querysheet.http
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
######################################
|
||||||
|
# Fling Querysheet for restclient.el #
|
||||||
|
######################################
|
||||||
|
|
||||||
|
# Authenticate as user
|
||||||
|
POST http://localhost:8080/api/auth/user
|
||||||
|
Content-Type: application/json
|
||||||
|
{"shareId": "shareId", "authCode":"secret"}
|
||||||
|
-> jq-set-var :token .
|
||||||
|
|
||||||
|
# :token = Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTQ0NjEzNzMsImV4cCI6MTU5NDY0MTM3Mywic3ViIjoiYWRtaW4ifQ.yu6sF1aE6sW4Jx1hBMj6iUsy8xfiaRGlIFVnHK4YkU8
|
||||||
|
|
||||||
|
# Authenticate as admin
|
||||||
|
POST http://localhost:8080/api/auth/admin
|
||||||
|
Content-Type: application/json
|
||||||
|
{"adminName": "admin", "adminPassword":"123"}
|
||||||
|
-> run-hook (restclient-set-var ":token" (buffer-substring-no-properties 1 (line-end-position)))
|
||||||
|
|
||||||
|
:token = Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTU2NzUxMjAsImV4cCI6MTU5NTg1NTEyMCwic3ViIjoiYWRtaW4ifQ.WzrGTTZTYHYOw8SskHQ2_sob2tzLIF6q8y8_2oyuafs
|
||||||
|
|
||||||
|
# Get all flings
|
||||||
|
GET http://localhost:8080/api/fling
|
||||||
|
Content-Type: application/json
|
||||||
|
:token
|
||||||
|
|
||||||
|
:flingid = 9f7353a3-efaa-41af-9f93-61e02dc5e440
|
||||||
|
|
||||||
|
# Put a fling
|
||||||
|
PUT http://localhost:8080/api/fling/:flingid
|
||||||
|
Content-Type: application/json
|
||||||
|
:token
|
||||||
|
{
|
||||||
|
"id": "9f7353a3-efaa-41af-9f93-61e02dc5e440",
|
||||||
|
"name": "Shared Fling from querysheetsdfasfd",
|
||||||
|
"creationTime": 1595253659362,
|
||||||
|
"shareId": "WWgTlNZJPZDQ6oowUYfxcQqq",
|
||||||
|
"directDownload": false,
|
||||||
|
"allowUpload": false,
|
||||||
|
"shared": true,
|
||||||
|
"expirationClicks": 12
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add a new fling
|
||||||
|
POST http://localhost:8080/api/fling
|
||||||
|
Content-Type: application/json
|
||||||
|
:token
|
||||||
|
{"name": "Shared Fling from querysheet", "expirationClicks": 12, "shared": true}
|
||||||
|
|
||||||
|
# Add a new fling
|
||||||
|
POST http://localhost:8080/api/fling
|
||||||
|
Content-Type: application/json
|
||||||
|
:token
|
||||||
|
{"name": "Unshared Fling from querysheet", "expirationClicks": 12, "shared": false}
|
||||||
|
|
||||||
|
# Add a new fling
|
||||||
|
POST http://localhost:8080/api/fling
|
||||||
|
Content-Type: application/json
|
||||||
|
:token
|
||||||
|
{"name": "Fling from querysheet with Auth", "expirationClicks": 12, "shared": true, "authCode": "abc"}
|
||||||
|
|
||||||
|
# Add a new fling
|
||||||
|
POST http://localhost:8080/api/fling
|
||||||
|
Content-Type: application/json
|
||||||
|
:token
|
||||||
|
{"name": "Fling from querysheet with Auth and very long name", "expirationClicks": 12, "shared": true, "authCode": "abc"}
|
||||||
|
|
||||||
|
# GET derived auth token
|
||||||
|
GET http://localhost:8080/api/auth/derive
|
||||||
|
Content-Type: application/json
|
||||||
|
:token
|
||||||
|
|
||||||
|
:derivedToken = 56c4ff2e-7da7-4582-bd2c-9a81d9a13abb
|
||||||
|
|
||||||
|
#
|
||||||
|
:flingId = 9f7353a3-efaa-41af-9f93-61e02dc5e440
|
||||||
|
|
||||||
|
# Get one fling
|
||||||
|
GET http://localhost:8080/api/fling/:flingId
|
||||||
|
:token
|
||||||
|
|
||||||
|
# Get all artifacts
|
||||||
|
GET http://localhost:8080/api/fling/:flingId/artifacts
|
||||||
|
Content-Type: application/json
|
||||||
|
:token
|
||||||
|
|
||||||
|
:artifactId = 01ba7fb9-9f2e-4809-9b2b-cbce12a92621
|
||||||
|
|
||||||
|
# Get artifact data by derived token
|
||||||
|
GET http://localhost:8080/api/artifacts/:artifactId/data?derivedtoken=:derivedToken
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
#
|
||||||
|
GET https://httpbin.org/json
|
||||||
|
-> jq-set-var :my-var .slideshow.slides[0].title
|
||||||
|
|
||||||
|
#
|
||||||
|
GET http://httpbin.org/ip
|
||||||
|
-> run-hook (restclient-set-var ":my-ip" (cdr (assq 'origin (json-read))))
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<profiles version="19">
|
<profiles version="19">
|
||||||
<profile kind="CodeFormatterProfile" name="GoogleStyle" version="19">
|
<profile kind="CodeFormatterProfile" name="FlingStyle" version="19">
|
||||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" value="insert"/>
|
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" value="insert"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
|
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression" value="do not insert"/>
|
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression" value="do not insert"/>
|
||||||
|
@ -142,7 +142,7 @@
|
||||||
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment" value="false"/>
|
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment" value="false"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="do not insert"/>
|
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="do not insert"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try" value="insert"/>
|
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try" value="insert"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line" value="false"/>
|
<setting id="org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line" value="true"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="insert"/>
|
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="insert"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment" value="false"/>
|
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment" value="false"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_relational_operator" value="0"/>
|
<setting id="org.eclipse.jdt.core.formatter.alignment_for_relational_operator" value="0"/>
|
||||||
|
@ -301,7 +301,7 @@
|
||||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow" value="insert"/>
|
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow" value="insert"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/>
|
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.comment.indent_tag_description" value="false"/>
|
<setting id="org.eclipse.jdt.core.formatter.comment.indent_tag_description" value="false"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line" value="false"/>
|
<setting id="org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line" value="true"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_record_constructor" value="end_of_line"/>
|
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_record_constructor" value="end_of_line"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration" value="insert"/>
|
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration" value="insert"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
|
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
|
||||||
|
@ -315,7 +315,7 @@
|
||||||
<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_body" value="true"/>
|
<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_body" value="true"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_multiple_fields" value="16"/>
|
<setting id="org.eclipse.jdt.core.formatter.alignment_for_multiple_fields" value="16"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments" value="insert"/>
|
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments" value="insert"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line" value="false"/>
|
<setting id="org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line" value="true"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
|
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_array_initializer" value="end_of_line"/>
|
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_array_initializer" value="end_of_line"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.wrap_before_logical_operator" value="true"/>
|
<setting id="org.eclipse.jdt.core.formatter.wrap_before_logical_operator" value="true"/>
|
||||||
|
@ -332,7 +332,7 @@
|
||||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/>
|
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
|
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations" value="do not insert"/>
|
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations" value="do not insert"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.keep_simple_do_while_body_on_same_line" value="false"/>
|
<setting id="org.eclipse.jdt.core.formatter.keep_simple_do_while_body_on_same_line" value="true"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration" value="insert"/>
|
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration" value="insert"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference" value="do not insert"/>
|
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference" value="do not insert"/>
|
||||||
<setting id="org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line" value="one_line_never"/>
|
<setting id="org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line" value="one_line_never"/>
|
||||||
|
|
22
scripts/release.sh
Executable file
22
scripts/release.sh
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
SCRIPT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# Go to project root
|
||||||
|
cd ${SCRIPT_DIR}
|
||||||
|
cd ..
|
||||||
|
echo "Working directory: $(pwd)"
|
||||||
|
|
||||||
|
# Determine versions
|
||||||
|
CUR_VERSION=$(cat VERSION)
|
||||||
|
NEW_VERSION=${1}
|
||||||
|
echo "Replacing ${CUR_VERSION} with ${NEW_VERSION}"
|
||||||
|
|
||||||
|
# Replace all versions
|
||||||
|
sed -i "s/${CUR_VERSION^^}/${NEW_VERSION^^}/g" service/fling/pom.xml
|
||||||
|
sed -i "s/${CUR_VERSION,,}/${NEW_VERSION,,}/g" service/fling/src/main/resources/*.yml
|
||||||
|
sed -i "s/${CUR_VERSION,,}/${NEW_VERSION,,}/g" .drone.yml
|
||||||
|
sed -i "s/${CUR_VERSION^^}/${NEW_VERSION^^}/g" .drone.yml
|
||||||
|
sed -i "s/${CUR_VERSION,,}/${NEW_VERSION,,}/g" web/fling/package.json
|
||||||
|
sed -i "s/${CUR_VERSION}/${NEW_VERSION}/g" VERSION
|
|
@ -11,7 +11,7 @@
|
||||||
</parent>
|
</parent>
|
||||||
<groupId>net.friedl</groupId>
|
<groupId>net.friedl</groupId>
|
||||||
<artifactId>fling</artifactId>
|
<artifactId>fling</artifactId>
|
||||||
<version>0.0.1-SNAPSHOT</version>
|
<version>0.1.0-SNAPSHOT</version>
|
||||||
<name>fling</name>
|
<name>fling</name>
|
||||||
<description>Simple artifact sharing</description>
|
<description>Simple artifact sharing</description>
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@
|
||||||
<bouncycastle.version>1.64</bouncycastle.version>
|
<bouncycastle.version>1.64</bouncycastle.version>
|
||||||
<jwt.version>0.11.1</jwt.version>
|
<jwt.version>0.11.1</jwt.version>
|
||||||
<spring.version>${project.parent.version}</spring.version>
|
<spring.version>${project.parent.version}</spring.version>
|
||||||
|
<fling.api.version>${project.version}</fling.api.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
@ -90,19 +91,24 @@
|
||||||
<artifactId>mapstruct</artifactId>
|
<artifactId>mapstruct</artifactId>
|
||||||
<version>${mapstruct.version}</version>
|
<version>${mapstruct.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- OpenAPI Generator -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mapstruct</groupId>
|
<groupId>org.springdoc</groupId>
|
||||||
<artifactId>mapstruct-processor</artifactId>
|
<artifactId>springdoc-openapi-ui</artifactId>
|
||||||
<version>${mapstruct.version}</version>
|
<version>1.2.32</version>
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<resources>
|
<resources>
|
||||||
<resource>
|
<resource>
|
||||||
<!-- Replace @spring.profiles.active@ in application.yml by setting in
|
<!--
|
||||||
maven profile See also: profiles section -->
|
Replace @...@ placeholder in resources.
|
||||||
|
Replaces:
|
||||||
|
- application.yml | @spring.profiles.active@ | see also: profiles section
|
||||||
|
- application.yml | @fling.api.version@ | see also: properties
|
||||||
|
-->
|
||||||
<directory>src/main/resources</directory>
|
<directory>src/main/resources</directory>
|
||||||
<filtering>true</filtering>
|
<filtering>true</filtering>
|
||||||
</resource>
|
</resource>
|
||||||
|
@ -111,7 +117,23 @@
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>pre-integration-test</id>
|
||||||
|
<goals>
|
||||||
|
<goal>start</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>post-integration-test</id>
|
||||||
|
<goals>
|
||||||
|
<goal>stop</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
|
<!-- Generators -->
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
@ -137,6 +159,29 @@
|
||||||
</annotationProcessorPaths>
|
</annotationProcessorPaths>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
|
<!-- OpenApi -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-maven-plugin</artifactId>
|
||||||
|
<version>1.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>integration-test</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>generate</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<outputFileName>flingapi.json</outputFileName>
|
||||||
|
<outputDir>${project.build.directory}/openapi-spec</outputDir>
|
||||||
|
<!-- Attach api doc to deployed artifact -->
|
||||||
|
<attachArtifact>true</attachArtifact>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<!-- Deployment -->
|
||||||
<plugin>
|
<plugin>
|
||||||
<artifactId>maven-deploy-plugin</artifactId>
|
<artifactId>maven-deploy-plugin</artifactId>
|
||||||
<executions>
|
<executions>
|
||||||
|
@ -165,7 +210,8 @@
|
||||||
<id>local</id>
|
<id>local</id>
|
||||||
<properties>
|
<properties>
|
||||||
<spring.profiles.active>local</spring.profiles.active>
|
<spring.profiles.active>local</spring.profiles.active>
|
||||||
<!-- automatically run annotation processors within the incremental compilation -->
|
<!-- automatically run annotation processors within the incremental
|
||||||
|
compilation -->
|
||||||
<m2e.apt.activation>jdt_apt</m2e.apt.activation>
|
<m2e.apt.activation>jdt_apt</m2e.apt.activation>
|
||||||
</properties>
|
</properties>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|
|
@ -1,30 +1,41 @@
|
||||||
package net.friedl.fling;
|
package net.friedl.fling;
|
||||||
|
|
||||||
import java.security.MessageDigest;
|
import java.nio.file.Path;
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
import net.friedl.fling.model.json.PathDeserializer;
|
||||||
|
import net.friedl.fling.model.json.PathSerializer;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class FlingConfiguration {
|
public class FlingConfiguration {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public MessageDigest keyHashDigest() throws NoSuchAlgorithmException {
|
public PasswordEncoder passwordEncoder() {
|
||||||
return MessageDigest.getInstance("SHA-512");
|
return new BCryptPasswordEncoder();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public ObjectMapper objectMapper() {
|
public ObjectMapper objectMapper() {
|
||||||
|
SimpleModule simpleModule = new SimpleModule();
|
||||||
|
simpleModule
|
||||||
|
.addDeserializer(Path.class, new PathDeserializer())
|
||||||
|
.addSerializer(Path.class, new PathSerializer());
|
||||||
|
|
||||||
ObjectMapper objectMapper = new ObjectMapper()
|
ObjectMapper objectMapper = new ObjectMapper()
|
||||||
.setSerializationInclusion(Include.NON_ABSENT)
|
.setSerializationInclusion(Include.NON_ABSENT)
|
||||||
.registerModule(new JavaTimeModule())
|
.registerModule(new JavaTimeModule())
|
||||||
// Handle instant as milliseconds
|
// Handle instant as milliseconds
|
||||||
.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
|
.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
|
||||||
.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false);
|
.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
|
||||||
|
.registerModule(simpleModule);
|
||||||
|
|
||||||
return objectMapper;
|
return objectMapper;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
package net.friedl.fling.security;
|
package net.friedl.fling;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
import java.util.List;
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
@ -11,30 +10,22 @@ import io.jsonwebtoken.Jwts;
|
||||||
import io.jsonwebtoken.security.Keys;
|
import io.jsonwebtoken.security.Keys;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@Data
|
@Data
|
||||||
|
@Configuration
|
||||||
@ConfigurationProperties("fling.security")
|
@ConfigurationProperties("fling.security")
|
||||||
public class FlingSecurityConfiguration {
|
public class FlingSecurityConfiguration {
|
||||||
private List<String> allowedOrigins;
|
|
||||||
|
|
||||||
private String adminUser;
|
|
||||||
|
|
||||||
private String adminPassword;
|
|
||||||
|
|
||||||
private String signingKey;
|
private String signingKey;
|
||||||
|
|
||||||
private Long jwtExpiration;
|
@Bean
|
||||||
|
public JwtParser jwtParser() {
|
||||||
|
return Jwts.parserBuilder()
|
||||||
|
.setSigningKey(jwtSigningKey())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public Key jwtSigningKey() {
|
public Key jwtSigningKey() {
|
||||||
byte[] key = signingKey.getBytes(StandardCharsets.UTF_8);
|
byte[] key = signingKey.getBytes(StandardCharsets.UTF_8);
|
||||||
return Keys.hmacShaKeyFor(key);
|
return Keys.hmacShaKeyFor(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
public JwtParser jwtParser(Key jwtSignigKey) {
|
|
||||||
return Jwts.parserBuilder()
|
|
||||||
.setSigningKey(jwtSignigKey)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package net.friedl.fling.controller;
|
package net.friedl.fling.controller;
|
||||||
|
|
||||||
import java.util.List;
|
import java.io.IOException;
|
||||||
|
import java.util.UUID;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.core.io.InputStreamResource;
|
import org.springframework.core.io.InputStreamResource;
|
||||||
|
@ -8,75 +9,70 @@ import org.springframework.core.io.Resource;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PatchMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
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.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import net.friedl.fling.model.dto.ArtifactDto;
|
import net.friedl.fling.model.dto.ArtifactDto;
|
||||||
import net.friedl.fling.persistence.archive.ArchiveException;
|
|
||||||
import net.friedl.fling.service.ArtifactService;
|
import net.friedl.fling.service.ArtifactService;
|
||||||
|
import net.friedl.fling.service.archive.ArchiveService;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api")
|
@RequestMapping("/api/artifacts")
|
||||||
|
@Tag(name = "artifact", description = "Operations on /api/artifacts")
|
||||||
|
@SecurityRequirement(name = "bearer")
|
||||||
|
@Validated
|
||||||
public class ArtifactController {
|
public class ArtifactController {
|
||||||
|
|
||||||
private ArtifactService artifactService;
|
private ArtifactService artifactService;
|
||||||
|
private ArchiveService archiveService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public ArtifactController(ArtifactService artifactService) {
|
public ArtifactController(ArtifactService artifactService, ArchiveService archiveService) {
|
||||||
this.artifactService = artifactService;
|
this.artifactService = artifactService;
|
||||||
|
this.archiveService = archiveService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(path = "/artifacts", params = "flingId")
|
@ApiResponse(responseCode = "404", description = "No artifact with `id` found")
|
||||||
public List<ArtifactDto> getArtifacts(@RequestParam Long flingId) {
|
@GetMapping(path = "/{id}")
|
||||||
return artifactService.findAllArtifacts(flingId);
|
public ArtifactDto getArtifact(@PathVariable UUID id) {
|
||||||
|
return artifactService.getById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(path = "/artifacts", params = "artifactId")
|
@ApiResponse(responseCode = "404", description = "No artifact with `id` found")
|
||||||
public ResponseEntity<ArtifactDto> getArtifact(@RequestParam Long artifactId) {
|
@DeleteMapping(path = "/{id}")
|
||||||
return ResponseEntity.of(artifactService.findArtifact(artifactId));
|
public void deleteArtifact(@PathVariable UUID id) throws IOException {
|
||||||
|
artifactService.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/artifacts/{flingId}")
|
@RequestBody(content = @Content(schema = @Schema(type = "string", format = "binary")))
|
||||||
public ArtifactDto postArtifact(@PathVariable Long flingId, HttpServletRequest request)
|
@PostMapping(path = "/{id}/data")
|
||||||
throws Exception {
|
public void uploadArtifactData(@PathVariable UUID id, HttpServletRequest request)
|
||||||
return artifactService.storeArtifact(flingId, request.getInputStream());
|
throws IOException {
|
||||||
|
archiveService.storeArtifact(id, request.getInputStream());
|
||||||
}
|
}
|
||||||
|
|
||||||
@PatchMapping(path = "/artifacts/{artifactId}", consumes = MediaType.APPLICATION_JSON_VALUE)
|
@ApiResponse(responseCode = "200",
|
||||||
public ArtifactDto patchArtifact(@PathVariable Long artifactId, @RequestBody String body) {
|
content = @Content(schema = @Schema(type = "string", format = "binary")))
|
||||||
return artifactService.mergeArtifact(artifactId, body);
|
@GetMapping(path = "/{id}/data")
|
||||||
}
|
public ResponseEntity<Resource> downloadArtifact(@PathVariable UUID id) throws IOException {
|
||||||
|
ArtifactDto artifactDto = artifactService.getById(id);
|
||||||
@DeleteMapping(path = "/artifacts/{artifactId}")
|
InputStreamResource data = new InputStreamResource(archiveService.getArtifact(id));
|
||||||
public void deleteArtifact(@PathVariable Long artifactId) throws ArchiveException {
|
|
||||||
artifactService.deleteArtifact(artifactId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping(path = "/artifacts/{artifactId}/downloadid")
|
|
||||||
public String getDownloadId(@PathVariable Long artifactId) {
|
|
||||||
return artifactService.generateDownloadId(artifactId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping(path = "/artifacts/{artifactId}/{downloadId}/download")
|
|
||||||
public ResponseEntity<Resource> downloadArtifact(@PathVariable Long artifactId,
|
|
||||||
@PathVariable String downloadId)
|
|
||||||
throws ArchiveException {
|
|
||||||
|
|
||||||
var artifact = artifactService.findArtifact(artifactId).orElseThrow();
|
|
||||||
var stream = new InputStreamResource(artifactService.downloadArtifact(downloadId));
|
|
||||||
|
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||||
"attachment;filename=\"" + artifact.getName() + "\"")
|
"attachment;filename=\"" + artifactDto.getPath().getFileName() + "\"")
|
||||||
.contentLength(artifact.getSize())
|
|
||||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
.body(stream);
|
.body(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
package net.friedl.fling.controller;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
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.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import net.friedl.fling.model.dto.AdminAuthDto;
|
||||||
|
import net.friedl.fling.model.dto.UserAuthDto;
|
||||||
|
import net.friedl.fling.security.FlingWebSecurityConfigurer;
|
||||||
|
import net.friedl.fling.service.AuthenticationService;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/auth")
|
||||||
|
@Tag(name = "auth", description = "Operations on /api/auth")
|
||||||
|
@Validated
|
||||||
|
public class AuthenticationController {
|
||||||
|
|
||||||
|
private AuthenticationService authenticationService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public AuthenticationController(AuthenticationService authenticationService) {
|
||||||
|
this.authenticationService = authenticationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(description = "Authenticates the fling admin by username and password")
|
||||||
|
@ApiResponse(responseCode = "200",
|
||||||
|
description = "JWT Token authenticating the admin of this fling instance")
|
||||||
|
@ApiResponse(responseCode = "403",
|
||||||
|
description = "Authentication failed, username or password are wrong")
|
||||||
|
@PostMapping(path = "/admin")
|
||||||
|
public String authenticateOwner(@RequestBody AdminAuthDto adminAuthDto) {
|
||||||
|
return authenticationService.authenticate(adminAuthDto)
|
||||||
|
.orElseThrow(() -> new AccessDeniedException("Wrong username or password"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(description = "Authenticates a fling user for a fling via code")
|
||||||
|
@ApiResponse(responseCode = "200",
|
||||||
|
description = "JWT Token authenticating the user for a fling")
|
||||||
|
@ApiResponse(responseCode = "403",
|
||||||
|
description = "Authentication failed, the provided code for the fling is wrong")
|
||||||
|
@ApiResponse(responseCode = "404",
|
||||||
|
description = "No fling for the given share id exists")
|
||||||
|
@PostMapping("/user")
|
||||||
|
public String authenticateUser(@RequestBody UserAuthDto userAuthDto) {
|
||||||
|
return authenticationService.authenticate(userAuthDto)
|
||||||
|
.orElseThrow(() -> new AccessDeniedException("Wrong username or password"));
|
||||||
|
}
|
||||||
|
|
||||||
|
//@formatter:off
|
||||||
|
/**
|
||||||
|
* Note that this endpoint is not protected. But the token will only get authority of the
|
||||||
|
* authenticated user.
|
||||||
|
* @see FlingWebSecurityConfigurer
|
||||||
|
* @see AuthenticationService
|
||||||
|
*/
|
||||||
|
@Operation(description = "Generate a derived token from the current authorization")
|
||||||
|
@ApiResponse(responseCode = "200", description = "Token impersonating the user")
|
||||||
|
@SecurityRequirement(name = "bearer")
|
||||||
|
@GetMapping("/derive")
|
||||||
|
public String deriveToken(
|
||||||
|
@Parameter(allowEmptyValue = true, description = "Token can only be used for authorizing one request. Defaults to true")
|
||||||
|
@RequestParam Optional<Boolean> singleUse)
|
||||||
|
{
|
||||||
|
return authenticationService.deriveToken(singleUse.orElse(true));
|
||||||
|
}
|
||||||
|
//@formatter:on
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package net.friedl.fling.controller;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import javax.persistence.EntityNotFoundException;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.context.request.WebRequest;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@ControllerAdvice
|
||||||
|
public class CommonExceptionHandler extends ResponseEntityExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler({EntityNotFoundException.class, IOException.class, UncheckedIOException.class})
|
||||||
|
public ResponseEntity<Object> handleNotFound(Exception ex, WebRequest request)
|
||||||
|
throws Exception {
|
||||||
|
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
|
||||||
|
if (ex instanceof IOException) {
|
||||||
|
log.error("IO Error", ex);
|
||||||
|
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
return handleExceptionInternal(ex, null, headers, status, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ex instanceof UncheckedIOException) {
|
||||||
|
log.error("IO Error", ex);
|
||||||
|
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
return handleExceptionInternal(ex, null, headers, status, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ex instanceof EntityNotFoundException) {
|
||||||
|
log.error("Entity not found", ex);
|
||||||
|
HttpStatus status = HttpStatus.NOT_FOUND;
|
||||||
|
return handleExceptionInternal(ex, null, headers, status, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.handleException(ex, request);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,9 @@ package net.friedl.fling.controller;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import javax.validation.Valid;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.core.io.InputStreamResource;
|
import org.springframework.core.io.InputStreamResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
|
@ -15,76 +18,99 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.PutMapping;
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import net.friedl.fling.model.dto.ArtifactDto;
|
||||||
import net.friedl.fling.model.dto.FlingDto;
|
import net.friedl.fling.model.dto.FlingDto;
|
||||||
import net.friedl.fling.persistence.archive.ArchiveException;
|
import net.friedl.fling.service.ArtifactService;
|
||||||
import net.friedl.fling.service.FlingService;
|
import net.friedl.fling.service.FlingService;
|
||||||
|
import net.friedl.fling.service.archive.ArchiveService;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api")
|
@RequestMapping("/api/fling")
|
||||||
|
@Tag(name = "fling", description = "Operations on /api/fling")
|
||||||
|
@SecurityRequirement(name = "bearer")
|
||||||
public class FlingController {
|
public class FlingController {
|
||||||
|
|
||||||
private FlingService flingService;
|
private FlingService flingService;
|
||||||
|
private ArtifactService artifactService;
|
||||||
|
private ArchiveService archiveService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public FlingController(FlingService flingService) {
|
public FlingController(FlingService flingService, ArtifactService artifactService,
|
||||||
|
ArchiveService archiveService) {
|
||||||
|
|
||||||
this.flingService = flingService;
|
this.flingService = flingService;
|
||||||
|
this.artifactService = artifactService;
|
||||||
|
this.archiveService = archiveService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/fling")
|
@GetMapping
|
||||||
public List<FlingDto> getFlings() {
|
public List<FlingDto> getFlings() {
|
||||||
return flingService.findAll();
|
return flingService.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/fling")
|
@PostMapping
|
||||||
public Long postFling(@RequestBody FlingDto flingDto) {
|
public FlingDto postFling(@RequestBody @Valid FlingDto flingDto) {
|
||||||
return flingService.createFling(flingDto);
|
return flingService.create(flingDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/fling/{flingId}")
|
@PutMapping("/{id}")
|
||||||
public void putFling(@PathVariable Long flingId, @RequestBody FlingDto flingDto) {
|
public FlingDto putFling(@PathVariable UUID id, @RequestBody @Valid FlingDto flingDto) {
|
||||||
flingService.mergeFling(flingId, flingDto);
|
return flingService.replace(id, flingDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(path = "/fling", params = "flingId")
|
@PostMapping("/{id}/artifacts")
|
||||||
public ResponseEntity<FlingDto> getFling(@RequestParam Long flingId) {
|
public ArtifactDto postArtifact(@PathVariable UUID id,
|
||||||
return ResponseEntity.of(flingService.findFlingById(flingId));
|
@RequestBody @Valid ArtifactDto artifactDto) {
|
||||||
|
return artifactService.create(id, artifactDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(path = "/fling", params = "shareId")
|
@GetMapping("/{id}/artifacts")
|
||||||
public ResponseEntity<FlingDto> getFlingByShareId(@RequestParam String shareId) {
|
public Set<ArtifactDto> getArtifacts(@PathVariable UUID id) {
|
||||||
return ResponseEntity.of(flingService.findFlingByShareId(shareId));
|
return flingService.getArtifacts(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(path = "/fling/shareExists/{shareId}")
|
@GetMapping(path = "/{id}")
|
||||||
public Boolean getShareExists(@PathVariable String shareId) {
|
public FlingDto getFling(@PathVariable UUID id) {
|
||||||
return flingService.existsShareUrl(shareId);
|
return flingService.getById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/fling/{flingId}")
|
@GetMapping(path = "/share/{shareId}")
|
||||||
public void deleteFling(@PathVariable Long flingId) {
|
public FlingDto getFlingByShareId(@PathVariable String shareId) {
|
||||||
flingService.deleteFlingById(flingId);
|
return flingService.getByShareId(shareId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(path = "/fling/{flingId}/package")
|
@DeleteMapping("/{id}")
|
||||||
public String packageFling(@PathVariable Long flingId) throws IOException, ArchiveException {
|
public void deleteFling(@PathVariable UUID id) throws IOException {
|
||||||
return flingService.packageFling(flingId);
|
flingService.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(path = "/fling/{flingId}/download/{downloadId}")
|
@Operation(responses = {
|
||||||
public ResponseEntity<Resource> downloadFling(@PathVariable Long flingId,
|
@ApiResponse(responseCode = "200",
|
||||||
@PathVariable String downloadId) throws ArchiveException, IOException {
|
content = @Content(
|
||||||
var fling = flingService.findFlingById(flingId).orElseThrow();
|
mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE,
|
||||||
var flingPackage = flingService.downloadFling(downloadId);
|
schema = @Schema(type = "string", format = "binary")))
|
||||||
var stream = new InputStreamResource(flingPackage.getFirst());
|
})
|
||||||
|
@GetMapping(path = "/{id}/data")
|
||||||
|
public ResponseEntity<Resource> getFlingData(@PathVariable UUID id) throws IOException {
|
||||||
|
FlingDto flingDto = flingService.getById(id);
|
||||||
|
InputStreamResource data = new InputStreamResource(archiveService.getFling(id));
|
||||||
|
Long length = data.contentLength();
|
||||||
|
|
||||||
|
data = new InputStreamResource(archiveService.getFling(id));
|
||||||
|
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||||
"attachment;filename=\"" + fling.getName() + ".zip" + "\"")
|
"attachment;filename=\"" + flingDto.getName() + ".zip" + "\"")
|
||||||
.contentLength(flingPackage.getSecond())
|
.contentLength(length)
|
||||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
.body(stream);
|
.body(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
package net.friedl.fling.controller;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import io.swagger.v3.oas.models.Components;
|
||||||
|
import io.swagger.v3.oas.models.OpenAPI;
|
||||||
|
import io.swagger.v3.oas.models.info.Contact;
|
||||||
|
import io.swagger.v3.oas.models.info.Info;
|
||||||
|
import io.swagger.v3.oas.models.info.License;
|
||||||
|
import io.swagger.v3.oas.models.media.Schema;
|
||||||
|
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||||
|
import io.swagger.v3.oas.models.security.SecurityScheme.Type;
|
||||||
|
import io.swagger.v3.oas.models.servers.Server;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties("fling.api")
|
||||||
|
public class OpenApiConfiguration {
|
||||||
|
|
||||||
|
private String version;
|
||||||
|
private String serverUrl;
|
||||||
|
private String serverDescription;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public OpenAPI openApi() {
|
||||||
|
OpenAPI openApi = new OpenAPI()
|
||||||
|
.components(new Components()
|
||||||
|
.addSecuritySchemes("bearer", bearerScheme())
|
||||||
|
.addSchemas("resource", new Schema<Resource>()
|
||||||
|
.type("string").format("binary")))
|
||||||
|
.info(apiInfo());
|
||||||
|
|
||||||
|
serverItem().ifPresent(openApi::addServersItem);
|
||||||
|
|
||||||
|
return openApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SecurityScheme bearerScheme() {
|
||||||
|
return new SecurityScheme()
|
||||||
|
.name("bearerAuth")
|
||||||
|
.type(Type.HTTP)
|
||||||
|
.scheme("bearer")
|
||||||
|
.bearerFormat("JWT");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Info apiInfo() {
|
||||||
|
return new Info()
|
||||||
|
.contact(new Contact()
|
||||||
|
.name("Armin Friedl")
|
||||||
|
.url("https://www.friedl.net")
|
||||||
|
.email("dev@friedl.net"))
|
||||||
|
.license(new License()
|
||||||
|
.name("The MIT License (MIT)"))
|
||||||
|
.title("The Fling API")
|
||||||
|
.description("Share file collections with expiration, protection and short urls")
|
||||||
|
.version(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Server> serverItem() {
|
||||||
|
if (serverUrl == null) return Optional.empty();
|
||||||
|
|
||||||
|
return Optional.of(
|
||||||
|
new Server()
|
||||||
|
.description(serverDescription)
|
||||||
|
.url(serverUrl));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package net.friedl.fling.model.dto;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Schema(name = "AdminAuth")
|
||||||
|
public class AdminAuthDto {
|
||||||
|
@NotNull
|
||||||
|
private String adminName;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private String adminPassword;
|
||||||
|
}
|
|
@ -1,23 +1,36 @@
|
||||||
package net.friedl.fling.model.dto;
|
package net.friedl.fling.model.dto;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Schema(name = "Artifact")
|
||||||
public class ArtifactDto {
|
public class ArtifactDto {
|
||||||
private String name;
|
@Schema(accessMode = AccessMode.READ_ONLY, type = "string")
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
private Long id;
|
@Schema(type = "string",
|
||||||
|
description = "Path of the artifact")
|
||||||
|
@NotNull
|
||||||
|
private Path path;
|
||||||
|
|
||||||
private String path;
|
@Schema(type = "integer", format = "int64", accessMode = AccessMode.READ_ONLY,
|
||||||
|
description = "Creation time in milliseconds since the unix epoch 01.01.1970 00:00:00 UTC")
|
||||||
|
private Instant creationTime;
|
||||||
|
|
||||||
private String doi;
|
@Schema(accessMode = AccessMode.READ_ONLY, type = "boolean",
|
||||||
|
description = "Whether the artifact was successfully persisted in the archive.")
|
||||||
private Long size;
|
@Builder.Default
|
||||||
|
private Boolean archived = false;
|
||||||
private Integer version;
|
|
||||||
|
|
||||||
private Instant uploadTime;
|
|
||||||
|
|
||||||
private FlingDto fling;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,96 +1,55 @@
|
||||||
package net.friedl.fling.model.dto;
|
package net.friedl.fling.model.dto;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.HashMap;
|
import java.util.UUID;
|
||||||
import java.util.Map;
|
import javax.validation.constraints.NotNull;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(name = "Fling")
|
||||||
public class FlingDto {
|
public class FlingDto {
|
||||||
|
@Schema(accessMode = AccessMode.READ_ONLY, type = "string")
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
@Schema(description = "Name of the fling")
|
||||||
|
@NotNull
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
private Long id;
|
@Schema(type = "integer", format = "int64", accessMode = AccessMode.READ_ONLY,
|
||||||
|
description = "Creation time in milliseconds since the unix epoch 01.01.1970 00:00:00 UTC")
|
||||||
private Instant creationTime;
|
private Instant creationTime;
|
||||||
|
|
||||||
@JsonIgnore
|
@Schema(description = "Share id of the fling. Used in the share link.")
|
||||||
private Boolean directDownload;
|
private String shareId;
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
private Boolean allowUpload;
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
private Boolean shared;
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
private String shareUrl;
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
private Integer expirationClicks;
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
private Instant expirationTime;
|
|
||||||
|
|
||||||
|
@Schema(description = "Authentication code for password protecting a fling.")
|
||||||
private String authCode;
|
private String authCode;
|
||||||
|
|
||||||
@JsonProperty("sharing")
|
@Schema(description = "Whether users should be redirected to fling download when accessing the "
|
||||||
private void unpackSharing(Map<String, Object> sharing) {
|
+ "fling by share id")
|
||||||
this.directDownload = (Boolean) sharing.getOrDefault("directDownload", false);
|
@Builder.Default
|
||||||
this.allowUpload = (Boolean) sharing.getOrDefault("allowUpload", false);
|
private Boolean directDownload = false;
|
||||||
this.shared = (Boolean) sharing.getOrDefault("shared", true);
|
|
||||||
this.shareUrl = (String) sharing.getOrDefault("shareUrl", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonProperty("sharing")
|
@Schema(description = "Allow uploads to the fling by users")
|
||||||
private Map<String, Object> packSharing() {
|
@Builder.Default
|
||||||
Map<String, Object> sharing = new HashMap<>();
|
private Boolean allowUpload = false;
|
||||||
sharing.put("directDownload", this.directDownload);
|
|
||||||
sharing.put("allowUpload", this.allowUpload);
|
|
||||||
sharing.put("shared", this.shared);
|
|
||||||
sharing.put("shareUrl", this.shareUrl);
|
|
||||||
|
|
||||||
return sharing;
|
@Schema(description = "Whether the fling is accessible by users via the share id")
|
||||||
}
|
@Builder.Default
|
||||||
|
private Boolean shared = true;
|
||||||
|
|
||||||
@JsonProperty("expiration")
|
@Schema(description = "How many clicks are left until the fling access by share id is disallowed")
|
||||||
private void unpackExpiration(Map<String, Object> expiration) {
|
private Integer expirationClicks;
|
||||||
String type = (String) expiration.getOrDefault("type", null);
|
|
||||||
if (type == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
switch (type) {
|
@Schema(type = "integer", format = "int64",
|
||||||
case "time":
|
description = "Expiration time in milliseconds since the unix epoch 01.01.1970 00:00:00 UTC")
|
||||||
this.expirationClicks = null;
|
private Instant expirationTime;
|
||||||
// json can only handle int, long must be given as string
|
|
||||||
// TODO: this back and forth conversion is a bit hack-ish
|
|
||||||
this.expirationTime =
|
|
||||||
Instant.ofEpochMilli(Long.valueOf(expiration.get("value").toString()));
|
|
||||||
break;
|
|
||||||
case "clicks":
|
|
||||||
this.expirationTime = null;
|
|
||||||
this.expirationClicks = Integer.valueOf(expiration.get("value").toString());
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Unexpected value '" + type + "'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonProperty("expiration")
|
|
||||||
private Map<String, Object> packExpiration() {
|
|
||||||
Map<String, Object> expiration = new HashMap<>();
|
|
||||||
|
|
||||||
if (this.expirationClicks != null) {
|
|
||||||
expiration.put("type", "clicks");
|
|
||||||
expiration.put("value", this.expirationClicks);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.expirationTime != null) {
|
|
||||||
expiration.put("type", "time");
|
|
||||||
expiration.put("value", this.expirationTime.toEpochMilli());
|
|
||||||
}
|
|
||||||
|
|
||||||
return expiration;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
package net.friedl.fling.model.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class FlingSharingDto {
|
|
||||||
private Boolean allowUpload;
|
|
||||||
|
|
||||||
private Boolean directDownload;
|
|
||||||
|
|
||||||
private String shareUrl;
|
|
||||||
}
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
package net.friedl.fling.model.dto;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Schema(name = "UserAuth")
|
||||||
|
public class UserAuthDto {
|
||||||
|
@NotNull
|
||||||
|
String shareId;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
String authCode;
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package net.friedl.fling.model.json;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.core.ObjectCodec;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||||
|
|
||||||
|
public class PathDeserializer extends StdDeserializer<Path> {
|
||||||
|
private static final long serialVersionUID = 1504807365764537418L;
|
||||||
|
|
||||||
|
public PathDeserializer() {
|
||||||
|
this(String.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected PathDeserializer(Class<?> vc) {
|
||||||
|
super(vc);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Path deserialize(JsonParser p, DeserializationContext ctxt)
|
||||||
|
throws IOException, JsonProcessingException {
|
||||||
|
|
||||||
|
ObjectCodec codec = p.getCodec();
|
||||||
|
JsonNode node = codec.readTree(p);
|
||||||
|
|
||||||
|
return Paths.get(node.textValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package net.friedl.fling.model.json;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
|
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
|
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||||
|
|
||||||
|
public class PathSerializer extends StdSerializer<Path> {
|
||||||
|
|
||||||
|
public PathSerializer() {
|
||||||
|
this(Path.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected PathSerializer(Class<Path> t) {
|
||||||
|
super(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final long serialVersionUID = -1003917305429893614L;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(Path value, JsonGenerator gen, SerializerProvider provider)
|
||||||
|
throws IOException {
|
||||||
|
gen.writeString(value.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,49 +1,20 @@
|
||||||
package net.friedl.fling.model.mapper;
|
package net.friedl.fling.model.mapper;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Set;
|
||||||
import java.util.Optional;
|
|
||||||
import org.mapstruct.Mapper;
|
import org.mapstruct.Mapper;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import net.friedl.fling.model.dto.ArtifactDto;
|
import net.friedl.fling.model.dto.ArtifactDto;
|
||||||
import net.friedl.fling.persistence.entities.ArtifactEntity;
|
import net.friedl.fling.persistence.entities.ArtifactEntity;
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Mapper(componentModel = "spring")
|
@Mapper(componentModel = "spring")
|
||||||
public abstract class ArtifactMapper {
|
public interface ArtifactMapper {
|
||||||
public abstract ArtifactDto map(ArtifactEntity artifactEntity);
|
ArtifactDto map(ArtifactEntity artifactEntity);
|
||||||
|
|
||||||
public abstract ArtifactEntity map(ArtifactDto artifactDto);
|
ArtifactEntity map(ArtifactDto artifactDto);
|
||||||
|
|
||||||
public abstract List<ArtifactDto> map(List<ArtifactEntity> artifactEntities);
|
List<ArtifactDto> mapEntities(List<ArtifactEntity> artifactEntities);
|
||||||
|
|
||||||
public Optional<ArtifactDto> map(Optional<ArtifactEntity> artifactEntity) {
|
Set<ArtifactDto> mapEntities(Set<ArtifactEntity> artifactEntities);
|
||||||
return artifactEntity.map(a -> map(a));
|
|
||||||
}
|
|
||||||
|
|
||||||
public ArtifactDto merge(ArtifactDto originalArtifactDto, Map<String, Object> patch) {
|
List<ArtifactEntity> mapDtos(List<ArtifactDto> artifactDtos);
|
||||||
ArtifactDto mergedArtifactDto = new ArtifactDto();
|
|
||||||
|
|
||||||
for (Field field : ArtifactDto.class.getDeclaredFields()) {
|
|
||||||
String fieldName = field.getName();
|
|
||||||
field.setAccessible(true);
|
|
||||||
try {
|
|
||||||
if (patch.containsKey(fieldName)) {
|
|
||||||
if (field.getType().equals(Long.class)) {
|
|
||||||
field.set(mergedArtifactDto, ((Number) patch.get(fieldName)).longValue());
|
|
||||||
}
|
|
||||||
field.set(mergedArtifactDto, patch.get(fieldName));
|
|
||||||
} else {
|
|
||||||
field.set(mergedArtifactDto, field.get(originalArtifactDto));
|
|
||||||
}
|
|
||||||
} catch (IllegalArgumentException | IllegalAccessException e) {
|
|
||||||
log.error("Could not merge {} [value={}] with {}", fieldName, patch.get(fieldName),
|
|
||||||
originalArtifactDto,
|
|
||||||
e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mergedArtifactDto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package net.friedl.fling.model.mapper;
|
package net.friedl.fling.model.mapper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
import org.mapstruct.Mapper;
|
import org.mapstruct.Mapper;
|
||||||
import net.friedl.fling.model.dto.FlingDto;
|
import net.friedl.fling.model.dto.FlingDto;
|
||||||
import net.friedl.fling.persistence.entities.FlingEntity;
|
import net.friedl.fling.persistence.entities.FlingEntity;
|
||||||
|
@ -10,11 +9,9 @@ import net.friedl.fling.persistence.entities.FlingEntity;
|
||||||
public interface FlingMapper {
|
public interface FlingMapper {
|
||||||
FlingDto map(FlingEntity flingEntity);
|
FlingDto map(FlingEntity flingEntity);
|
||||||
|
|
||||||
default Optional<FlingDto> map(Optional<FlingEntity> flingEntity) {
|
|
||||||
return flingEntity.map(f -> map(f));
|
|
||||||
}
|
|
||||||
|
|
||||||
FlingEntity map(FlingDto flingDto);
|
FlingEntity map(FlingDto flingDto);
|
||||||
|
|
||||||
List<FlingDto> map(List<FlingEntity> flingEntities);
|
List<FlingDto> mapEntities(List<FlingEntity> flingEntities);
|
||||||
|
|
||||||
|
List<FlingEntity> mapDtos(List<FlingDto> flingDtos);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
package net.friedl.fling.persistence.archive;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
public interface Archive {
|
|
||||||
/**
|
|
||||||
* Retrieve an artifact from the archive
|
|
||||||
*
|
|
||||||
* @param id The unique artifact id as returned by {@link Archive#store}
|
|
||||||
* @return An {@link InputStream} for reading the artifact
|
|
||||||
*/
|
|
||||||
InputStream get(String id) throws ArchiveException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store an artifact
|
|
||||||
*
|
|
||||||
* @param is The artifact represented as {@link InputStream}
|
|
||||||
* @return A unique archive id for the artifact
|
|
||||||
* @throws IOException If anything goes wrong while storing the artifact in the archive
|
|
||||||
*/
|
|
||||||
String store(InputStream is) throws ArchiveException;
|
|
||||||
|
|
||||||
default String store(File file) throws ArchiveException {
|
|
||||||
try {
|
|
||||||
return store(new FileInputStream(file));
|
|
||||||
} catch (IOException ex) {
|
|
||||||
throw new ArchiveException(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete an artifact
|
|
||||||
*
|
|
||||||
* @param id The unique artifact id as returned by {@link Archive#store}
|
|
||||||
*/
|
|
||||||
void remove(String id) throws ArchiveException;
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
package net.friedl.fling.persistence.archive;
|
|
||||||
|
|
||||||
public class ArchiveException extends Exception {
|
|
||||||
private static final long serialVersionUID = 6216735865308056261L;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new exception with {@code null} as its detail message. The cause is not
|
|
||||||
* initialized, and may subsequently be initialized by a call to {@link #initCause}.
|
|
||||||
*/
|
|
||||||
public ArchiveException() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new exception with the specified detail message. The cause is not initialized, and
|
|
||||||
* may subsequently be initialized by a call to {@link #initCause}.
|
|
||||||
*
|
|
||||||
* @param message the detail message. The detail message is saved for later retrieval by the
|
|
||||||
* {@link #getMessage()} method.
|
|
||||||
*/
|
|
||||||
public ArchiveException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new exception with the specified detail message and cause.
|
|
||||||
* <p>
|
|
||||||
* Note that the detail message associated with {@code cause} is <i>not</i> automatically
|
|
||||||
* incorporated in this exception's detail message.
|
|
||||||
*
|
|
||||||
* @param message the detail message (which is saved for later retrieval by the
|
|
||||||
* {@link #getMessage()} method).
|
|
||||||
* @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
|
|
||||||
* (A {@code null} value is permitted, and indicates that the cause is nonexistent or
|
|
||||||
* unknown.)
|
|
||||||
* @since 1.4
|
|
||||||
*/
|
|
||||||
public ArchiveException(String message, Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new exception with the specified cause and a detail message of
|
|
||||||
* {@code (cause==null ? null : cause.toString())} (which typically contains the class and detail
|
|
||||||
* message of {@code cause}). This constructor is useful for exceptions that are little more than
|
|
||||||
* wrappers for other throwables (for example,
|
|
||||||
* {@link java.security.PrivilegedActionArchiveException}).
|
|
||||||
*
|
|
||||||
* @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
|
|
||||||
* (A {@code null} value is permitted, and indicates that the cause is nonexistent or
|
|
||||||
* unknown.)
|
|
||||||
* @since 1.4
|
|
||||||
*/
|
|
||||||
public ArchiveException(Throwable cause) {
|
|
||||||
super(cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new exception with the specified detail message, cause, suppression enabled or
|
|
||||||
* disabled, and writable stack trace enabled or disabled.
|
|
||||||
*
|
|
||||||
* @param message the detail message.
|
|
||||||
* @param cause the cause. (A {@code null} value is permitted, and indicates that the cause is
|
|
||||||
* nonexistent or unknown.)
|
|
||||||
* @param enableSuppression whether or not suppression is enabled or disabled
|
|
||||||
* @param writableStackTrace whether or not the stack trace should be writable
|
|
||||||
* @since 1.7
|
|
||||||
*/
|
|
||||||
protected ArchiveException(String message, Throwable cause, boolean enableSuppression,
|
|
||||||
boolean writableStackTrace) {
|
|
||||||
super(message, cause, enableSuppression, writableStackTrace);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
package net.friedl.fling.persistence.archive.impl;
|
|
||||||
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.channels.FileChannel;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.nio.file.StandardOpenOption;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import net.friedl.fling.persistence.archive.Archive;
|
|
||||||
import net.friedl.fling.persistence.archive.ArchiveException;
|
|
||||||
|
|
||||||
@Component("fileSystemArchive")
|
|
||||||
public class FileSystemArchive implements Archive {
|
|
||||||
private MessageDigest fileStoreDigest;
|
|
||||||
|
|
||||||
private FileSystemArchiveConfiguration configuration;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public FileSystemArchive(MessageDigest fileStoreDigest,
|
|
||||||
FileSystemArchiveConfiguration configuration) {
|
|
||||||
this.fileStoreDigest = fileStoreDigest;
|
|
||||||
this.configuration = configuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InputStream get(String id) throws ArchiveException {
|
|
||||||
try {
|
|
||||||
var path = Paths.get(configuration.getDirectory(), id);
|
|
||||||
FileInputStream fis = new FileInputStream(path.toFile());
|
|
||||||
return fis;
|
|
||||||
} catch (FileNotFoundException ex) {
|
|
||||||
throw new ArchiveException(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String store(InputStream is) throws ArchiveException {
|
|
||||||
try {
|
|
||||||
byte[] fileBytes = is.readAllBytes();
|
|
||||||
is.close();
|
|
||||||
|
|
||||||
String fileStoreId = hexEncode(fileStoreDigest.digest(fileBytes));
|
|
||||||
|
|
||||||
FileChannel fc = FileChannel.open(Paths.get(configuration.getDirectory(), fileStoreId),
|
|
||||||
StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE,
|
|
||||||
StandardOpenOption.CREATE);
|
|
||||||
|
|
||||||
fc.write(ByteBuffer.wrap(fileBytes));
|
|
||||||
|
|
||||||
fc.close();
|
|
||||||
return fileStoreId;
|
|
||||||
|
|
||||||
} catch (IOException ex) {
|
|
||||||
throw new ArchiveException(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void remove(String id) throws ArchiveException {
|
|
||||||
var path = Paths.get(configuration.getDirectory(), id);
|
|
||||||
try {
|
|
||||||
Files.deleteIfExists(path);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new ArchiveException("Could not delete file at " + path.toString(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String hexEncode(byte[] fileStoreId) {
|
|
||||||
StringBuilder sb = new StringBuilder(fileStoreId.length * 2);
|
|
||||||
for (byte b : fileStoreId)
|
|
||||||
sb.append(String.format("%02x", b));
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
package net.friedl.fling.persistence.archive.impl;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import javax.annotation.PostConstruct;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Configuration
|
|
||||||
@ConfigurationProperties("fling.archive.fileystem")
|
|
||||||
@ConditionalOnBean(FileSystemArchive.class)
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
public class FileSystemArchiveConfiguration {
|
|
||||||
private String directory;
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public MessageDigest fileStoreDigest() throws NoSuchAlgorithmException {
|
|
||||||
return MessageDigest.getInstance("SHA-512");
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
public void init() throws IOException {
|
|
||||||
if (directory == null) {
|
|
||||||
log.info("Directory not configured take temp path");
|
|
||||||
Path tmpPath = Files.createTempDirectory("fling");
|
|
||||||
this.directory = tmpPath.toAbsolutePath().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("File store directory: {}", directory);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,46 +1,48 @@
|
||||||
package net.friedl.fling.persistence.entities;
|
package net.friedl.fling.persistence.entities;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
import javax.persistence.Column;
|
import javax.persistence.Column;
|
||||||
import javax.persistence.Entity;
|
import javax.persistence.Entity;
|
||||||
import javax.persistence.GeneratedValue;
|
import javax.persistence.GeneratedValue;
|
||||||
import javax.persistence.Id;
|
import javax.persistence.Id;
|
||||||
import javax.persistence.ManyToOne;
|
import javax.persistence.ManyToOne;
|
||||||
import javax.persistence.PrePersist;
|
|
||||||
import javax.persistence.Table;
|
import javax.persistence.Table;
|
||||||
|
import javax.persistence.UniqueConstraint;
|
||||||
|
import javax.persistence.Version;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
import org.hibernate.annotations.UpdateTimestamp;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "Artifact")
|
@Table(name = "Artifact", uniqueConstraints = @UniqueConstraint(columnNames = {"fling_id", "path"}))
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public class ArtifactEntity {
|
public class ArtifactEntity {
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue
|
@GeneratedValue
|
||||||
private Long id;
|
private UUID id;
|
||||||
|
|
||||||
private String name;
|
@Column(nullable = false)
|
||||||
|
private Path path;
|
||||||
|
|
||||||
private Integer version;
|
@Column(unique = true, nullable = true)
|
||||||
|
private String archiveId;
|
||||||
|
|
||||||
private String path;
|
@Column(nullable = false)
|
||||||
|
private Boolean archived = false;
|
||||||
@Column(unique = true)
|
|
||||||
private String doi;
|
|
||||||
|
|
||||||
private Instant uploadTime;
|
|
||||||
|
|
||||||
private Long size;
|
|
||||||
|
|
||||||
@ManyToOne(optional = false)
|
@ManyToOne(optional = false)
|
||||||
private FlingEntity fling;
|
private FlingEntity fling;
|
||||||
|
|
||||||
@PrePersist
|
@CreationTimestamp
|
||||||
private void prePersist() {
|
private Instant creationTime;
|
||||||
this.uploadTime = Instant.now();
|
|
||||||
|
|
||||||
if (this.version == null)
|
@UpdateTimestamp
|
||||||
this.version = -1;
|
private Instant updateTime;
|
||||||
}
|
|
||||||
|
@Version
|
||||||
|
private Long version;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
package net.friedl.fling.persistence.entities;
|
package net.friedl.fling.persistence.entities;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
import javax.persistence.CascadeType;
|
import javax.persistence.CascadeType;
|
||||||
import javax.persistence.Column;
|
import javax.persistence.Column;
|
||||||
import javax.persistence.Entity;
|
import javax.persistence.Entity;
|
||||||
import javax.persistence.GeneratedValue;
|
import javax.persistence.GeneratedValue;
|
||||||
import javax.persistence.Id;
|
import javax.persistence.Id;
|
||||||
import javax.persistence.OneToMany;
|
import javax.persistence.OneToMany;
|
||||||
import javax.persistence.PostPersist;
|
|
||||||
import javax.persistence.PrePersist;
|
|
||||||
import javax.persistence.Table;
|
import javax.persistence.Table;
|
||||||
|
import javax.persistence.Version;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
import org.hibernate.annotations.UpdateTimestamp;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@ -21,50 +24,37 @@ import lombok.Setter;
|
||||||
public class FlingEntity {
|
public class FlingEntity {
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue
|
@GeneratedValue
|
||||||
private Long id;
|
private UUID id;
|
||||||
|
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
private Instant creationTime;
|
|
||||||
|
|
||||||
private Instant expirationTime;
|
private Instant expirationTime;
|
||||||
|
|
||||||
private Integer expirationClicks;
|
private Integer expirationClicks;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private Boolean directDownload;
|
private Boolean directDownload = false;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private Boolean allowUpload;
|
private Boolean allowUpload = false;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private Boolean shared;
|
private Boolean shared = true;
|
||||||
|
|
||||||
@Column(unique = true, nullable = false)
|
@Column(unique = true, nullable = false)
|
||||||
private String shareUrl;
|
private String shareId;
|
||||||
|
|
||||||
private String authCode;
|
private String authCode;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "fling", cascade = CascadeType.ALL, orphanRemoval = true)
|
@OneToMany(mappedBy = "fling", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
private Set<ArtifactEntity> artifacts;
|
private Set<ArtifactEntity> artifacts;
|
||||||
|
|
||||||
@PrePersist
|
@CreationTimestamp
|
||||||
private void prePersist() {
|
private Date creationTime;
|
||||||
if (this.directDownload == null)
|
|
||||||
this.directDownload = false;
|
|
||||||
if (this.allowUpload == null)
|
|
||||||
this.allowUpload = false;
|
|
||||||
if (this.shared == null)
|
|
||||||
this.shared = true;
|
|
||||||
|
|
||||||
this.creationTime = Instant.now();
|
@UpdateTimestamp
|
||||||
}
|
private Date updateTime;
|
||||||
|
|
||||||
@PostPersist
|
@Version
|
||||||
private void postPersist() {
|
private Long version;
|
||||||
System.out.println("ID: " + this.id);
|
|
||||||
System.out.println("Share Url: " + this.shareUrl);
|
|
||||||
|
|
||||||
this.shareUrl = this.id + this.shareUrl;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
package net.friedl.fling.persistence.entities;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Id;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
import javax.persistence.Version;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
import org.hibernate.annotations.UpdateTimestamp;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "Token")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class TokenEntity {
|
||||||
|
@Id
|
||||||
|
private UUID id; // Note that this is not generated to ensure randomness independent from the
|
||||||
|
// persistence provider
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Boolean singleUse = true;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String token; // JWT token this token is derived from
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
private Instant creationTime;
|
||||||
|
|
||||||
|
@UpdateTimestamp
|
||||||
|
private Instant updateTime;
|
||||||
|
|
||||||
|
@Version
|
||||||
|
private Long version;
|
||||||
|
}
|
|
@ -1,14 +1,10 @@
|
||||||
package net.friedl.fling.persistence.repositories;
|
package net.friedl.fling.persistence.repositories;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.UUID;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import net.friedl.fling.persistence.entities.ArtifactEntity;
|
import net.friedl.fling.persistence.entities.ArtifactEntity;
|
||||||
|
|
||||||
public interface ArtifactRepository extends JpaRepository<ArtifactEntity, Long> {
|
public interface ArtifactRepository extends JpaRepository<ArtifactEntity, UUID> {
|
||||||
Optional<ArtifactEntity> findByDoi(String doi);
|
List<ArtifactEntity> findAllByFlingId(UUID flingId);
|
||||||
|
|
||||||
List<ArtifactEntity> deleteByDoi(String doi);
|
|
||||||
|
|
||||||
List<ArtifactEntity> findAllByFlingId(Long flingId);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
package net.friedl.fling.persistence.repositories;
|
package net.friedl.fling.persistence.repositories;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import net.friedl.fling.persistence.entities.FlingEntity;
|
import net.friedl.fling.persistence.entities.FlingEntity;
|
||||||
|
|
||||||
public interface FlingRepository extends JpaRepository<FlingEntity, Long> {
|
public interface FlingRepository extends JpaRepository<FlingEntity, UUID> {
|
||||||
Optional<FlingEntity> findByName(String name);
|
Optional<FlingEntity> findByName(String name);
|
||||||
|
|
||||||
Optional<FlingEntity> findByShareUrl(String shareUrl);
|
FlingEntity findByShareId(String shareId);
|
||||||
|
|
||||||
@Query("SELECT COUNT(*) FROM ArtifactEntity a, FlingEntity f where a.fling=f.id and f.id=:flingId")
|
@Query("SELECT fe FROM FlingEntity fe JOIN ArtifactEntity ae ON fe.id=ae.fling.id WHERE ae.id=:artifactId")
|
||||||
Long countArtifactsById(Long flingId);
|
FlingEntity findByArtifactId(UUID artifactId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package net.friedl.fling.persistence.repositories;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import net.friedl.fling.persistence.entities.TokenEntity;
|
||||||
|
|
||||||
|
public interface TokenRepository extends JpaRepository<TokenEntity, UUID> {
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package net.friedl.fling.persistence.types;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import javax.persistence.AttributeConverter;
|
||||||
|
import javax.persistence.Converter;
|
||||||
|
|
||||||
|
@Converter(autoApply = true)
|
||||||
|
public class PathConverter implements AttributeConverter<Path, String> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String convertToDatabaseColumn(Path attribute) {
|
||||||
|
return attribute.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Path convertToEntityAttribute(String dbData) {
|
||||||
|
return Paths.get(dbData);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,89 +0,0 @@
|
||||||
package net.friedl.fling.security;
|
|
||||||
|
|
||||||
import java.util.NoSuchElementException;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import net.friedl.fling.security.authentication.FlingToken;
|
|
||||||
import net.friedl.fling.security.authentication.dto.UserAuthDto;
|
|
||||||
import net.friedl.fling.service.ArtifactService;
|
|
||||||
import net.friedl.fling.service.FlingService;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
public class AuthorizationService {
|
|
||||||
private FlingService flingService;
|
|
||||||
private ArtifactService artifactService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public AuthorizationService(FlingService flingService, ArtifactService artifactService) {
|
|
||||||
this.flingService = flingService;
|
|
||||||
this.artifactService = artifactService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean allowUpload(Long flingId, AbstractAuthenticationToken token) {
|
|
||||||
if (!(token instanceof FlingToken))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
FlingToken flingToken = (FlingToken) token;
|
|
||||||
if (flingToken.getGrantedFlingAuthority().getAuthority()
|
|
||||||
.equals(FlingAuthority.FLING_OWNER.name())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var uploadAllowed = flingService.findFlingById(flingId).orElseThrow().getAllowUpload();
|
|
||||||
|
|
||||||
return uploadAllowed && flingToken.getGrantedFlingAuthority().getFlingId().equals(flingId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean allowPatchingArtifact(Long artifactId, FlingToken authentication) {
|
|
||||||
var flingId = artifactService.findArtifact(artifactId).orElseThrow().getFling().getId();
|
|
||||||
return allowUpload(flingId, authentication);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean allowFlingAccess(UserAuthDto userAuth, String shareUrl) {
|
|
||||||
return userAuth.getShareId().equals(shareUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean allowFlingAccess(Long flingId, AbstractAuthenticationToken token) {
|
|
||||||
if (!(token instanceof FlingToken))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
FlingToken flingToken = (FlingToken) token;
|
|
||||||
if (flingToken.getGrantedFlingAuthority().getAuthority()
|
|
||||||
.equals(FlingAuthority.FLING_OWNER.name())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return flingToken.getGrantedFlingAuthority().getFlingId().equals(flingId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean allowFlingAccess(AbstractAuthenticationToken token, HttpServletRequest request) {
|
|
||||||
if (!(token instanceof FlingToken))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
FlingToken flingToken = (FlingToken) token;
|
|
||||||
if (flingToken.getGrantedFlingAuthority().getAuthority()
|
|
||||||
.equals(FlingAuthority.FLING_OWNER.name())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var shareId = request.getParameter("shareId");
|
|
||||||
|
|
||||||
Long flingId;
|
|
||||||
|
|
||||||
try {
|
|
||||||
flingId = shareId != null
|
|
||||||
? flingService.findFlingByShareId(shareId).orElseThrow().getId()
|
|
||||||
: Long.parseLong(request.getParameter("flingId"));
|
|
||||||
} catch (NumberFormatException | NoSuchElementException e) {
|
|
||||||
log.warn("Invalid shareId [shareId=\"{}\"] or flingId [flingId=\"{}\"] found",
|
|
||||||
request.getParameter("shareId"), request.getParameter("flingId"));
|
|
||||||
flingId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return flingToken.getGrantedFlingAuthority().getFlingId().equals(flingId);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
package net.friedl.fling.security;
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
|
||||||
|
public enum FlingAuthorities {
|
||||||
|
FLING_ADMIN("admin"), FLING_USER("user"), FLING_TOKEN("token");
|
||||||
|
|
||||||
|
String authority;
|
||||||
|
|
||||||
|
FlingAuthorities(String authority) {
|
||||||
|
this.authority = authority;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean verify(String authority) {
|
||||||
|
return this.authority.equals(authority);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean verify(AbstractAuthenticationToken authenticationToken) {
|
||||||
|
return authenticationToken.getAuthorities().stream()
|
||||||
|
.map(GrantedAuthority::getAuthority)
|
||||||
|
.anyMatch(this.authority::equals);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean verify(GrantedAuthority grantedAuthority) {
|
||||||
|
return this.authority.equals(grantedAuthority.getAuthority());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthority() {
|
||||||
|
return authority;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +0,0 @@
|
||||||
package net.friedl.fling.security;
|
|
||||||
|
|
||||||
public enum FlingAuthority {
|
|
||||||
FLING_OWNER, FLING_USER
|
|
||||||
}
|
|
|
@ -1,46 +1,49 @@
|
||||||
package net.friedl.fling.security;
|
package net.friedl.fling.security;
|
||||||
|
|
||||||
|
import static net.friedl.fling.security.FlingAuthorities.FLING_ADMIN;
|
||||||
|
import static net.friedl.fling.security.FlingAuthorities.FLING_USER;
|
||||||
import static org.springframework.security.config.Customizer.withDefaults;
|
import static org.springframework.security.config.Customizer.withDefaults;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
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.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
|
||||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
|
||||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
|
||||||
import org.springframework.web.cors.CorsConfiguration;
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
import org.springframework.web.cors.CorsConfigurationSource;
|
import org.springframework.web.cors.CorsConfigurationSource;
|
||||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import net.friedl.fling.security.authentication.JwtAuthenticationFilter;
|
import net.friedl.fling.security.authentication.filter.BearerAuthenticationFilter;
|
||||||
|
import net.friedl.fling.security.authentication.filter.TokenAuthenticationFilter;
|
||||||
|
import net.friedl.fling.service.AuthorizationService;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Configuration
|
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
|
@ConfigurationProperties(prefix = "fling.security")
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
|
public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
|
||||||
private JwtAuthenticationFilter jwtAuthenticationFilter;
|
private List<String> allowedOrigins;
|
||||||
|
|
||||||
|
private TokenAuthenticationFilter tokenAuthenticationFilter;
|
||||||
|
private BearerAuthenticationFilter bearerAuthenticationFilter;
|
||||||
private AuthorizationService authorizationService;
|
private AuthorizationService authorizationService;
|
||||||
private FlingSecurityConfiguration securityConfiguration;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public FlingWebSecurityConfigurer(JwtAuthenticationFilter jwtAuthenticationFilter,
|
public FlingWebSecurityConfigurer(
|
||||||
AuthorizationService authorizationService,
|
TokenAuthenticationFilter tokenAuthenticationFilter,
|
||||||
FlingSecurityConfiguration securityConfiguraiton) {
|
BearerAuthenticationFilter bearerAuthenticationFilter,
|
||||||
|
AuthorizationService authorizationService) {
|
||||||
|
|
||||||
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
|
this.tokenAuthenticationFilter = tokenAuthenticationFilter;
|
||||||
|
this.bearerAuthenticationFilter = bearerAuthenticationFilter;
|
||||||
this.authorizationService = authorizationService;
|
this.authorizationService = authorizationService;
|
||||||
this.securityConfiguration = securityConfiguraiton;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -49,80 +52,98 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
|
||||||
http
|
http
|
||||||
.csrf().disable()
|
.csrf().disable()
|
||||||
.cors(withDefaults())
|
.cors(withDefaults())
|
||||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
|
.headers().frameOptions().disable().and()
|
||||||
// Everybody can try to authenticate
|
|
||||||
|
/**********************************************/
|
||||||
|
/** Authentication Interceptor Configuration **/
|
||||||
|
/**********************************************/
|
||||||
|
.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
|
||||||
|
.addFilterAfter(bearerAuthenticationFilter, TokenAuthenticationFilter.class)
|
||||||
|
// Do not keep authorization token in session. This would interfere with bearer authentication
|
||||||
|
// in that it is possible to authenticate without a bearer token if the session is kept.
|
||||||
|
// Turn off this confusing and non-obvious behavior.
|
||||||
|
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
|
||||||
|
|
||||||
|
|
||||||
|
/*************************************/
|
||||||
|
/** API Authorization Configuration **/
|
||||||
|
/*************************************/
|
||||||
|
//! Go from most specific to more !//
|
||||||
|
//! general, as first hit counts !//
|
||||||
|
|
||||||
|
/**********************************/
|
||||||
|
/** Authorization for: /api/auth **/
|
||||||
|
/**********************************/
|
||||||
|
.authorizeRequests()
|
||||||
|
.antMatchers("/api/auth/derive")
|
||||||
|
.hasAnyAuthority(FLING_ADMIN.getAuthority(), FLING_USER.getAuthority())
|
||||||
|
.and()
|
||||||
.authorizeRequests()
|
.authorizeRequests()
|
||||||
.antMatchers("/api/auth/**")
|
.antMatchers("/api/auth/**")
|
||||||
.permitAll()
|
.permitAll()
|
||||||
.and()
|
.and()
|
||||||
// We need to go from most specific to more general.
|
|
||||||
// Hence, first define user permissions
|
|
||||||
|
/***********************************/
|
||||||
|
/** Authorization for: /api/fling **/
|
||||||
|
/***********************************/
|
||||||
.authorizeRequests()
|
.authorizeRequests()
|
||||||
// TODO: This is still insecure since URLs are not encrypted
|
.antMatchers(HttpMethod.GET, "/api/fling/share/{shareId}")
|
||||||
// TODO: iframe requests don't send the bearer, use cookie instead
|
.access("@authorizationService.allowFlingAccessByShareId(#shareId, authentication)")
|
||||||
.antMatchers(HttpMethod.GET, "/api/fling/{flingId}/download/{downloadId}")
|
|
||||||
.permitAll()
|
|
||||||
.and()
|
|
||||||
.authorizeRequests()
|
|
||||||
.antMatchers(HttpMethod.POST, "/api/artifacts/{flingId}/**")
|
|
||||||
.access("@authorizationService.allowUpload(#flingId, authentication)")
|
|
||||||
.and()
|
|
||||||
.authorizeRequests()
|
|
||||||
.antMatchers(HttpMethod.PATCH, "/api/artifacts/{artifactId}")
|
|
||||||
.access("@authorizationService.allowPatchingArtifact(#artifactId, authentication)")
|
|
||||||
.and()
|
|
||||||
.authorizeRequests()
|
|
||||||
// TODO: This is still insecure since URLs are not encrypted
|
|
||||||
// TODO: iframe requests don't send the bearer, use cookie instead
|
|
||||||
.antMatchers("/api/artifacts/{artifactId}/{downloadId}/download")
|
|
||||||
.permitAll()
|
|
||||||
.and()
|
|
||||||
.authorizeRequests()
|
|
||||||
// TODO: Security by request parameters is just not well supported with spring security
|
|
||||||
// TODO: Change API
|
|
||||||
.regexMatchers(HttpMethod.GET, "\\/api\\/fling\\?(shareId=|flingId=)[a-zA-Z0-9]+")
|
|
||||||
.access("@authorizationService.allowFlingAccess(authentication, request)")
|
|
||||||
.and()
|
|
||||||
.authorizeRequests()
|
|
||||||
// TODO: Security by request parameters is just not well supported with spring security
|
|
||||||
// TODO: Change API
|
|
||||||
.regexMatchers(HttpMethod.GET, "\\/api\\/artifacts\\?(shareId=|flingId=)[a-zA-Z0-9]+")
|
|
||||||
.access("@authorizationService.allowFlingAccess(authentication, request)")
|
|
||||||
.and()
|
.and()
|
||||||
.authorizeRequests()
|
.authorizeRequests()
|
||||||
.antMatchers(HttpMethod.GET, "/api/fling/{flingId}/**")
|
.antMatchers(HttpMethod.GET, "/api/fling/{flingId}/**")
|
||||||
.access("@authorizationService.allowFlingAccess(#flingId, authentication)")
|
.access("@authorizationService.allowFlingAccess(#flingId, authentication)")
|
||||||
.and()
|
.and()
|
||||||
// And lastly, the owner is allowed everything
|
|
||||||
.authorizeRequests()
|
.authorizeRequests()
|
||||||
.antMatchers("/api/**")
|
.antMatchers(HttpMethod.POST, "/api/fling/{flingId}/artifact")
|
||||||
.hasAuthority(FlingAuthority.FLING_OWNER.name());
|
.access("@authorizationService.allowUpload(#flingId, authentication)")
|
||||||
|
.and()
|
||||||
|
// only admin can create, delete, list and modify flings
|
||||||
|
.authorizeRequests()
|
||||||
|
.antMatchers(HttpMethod.DELETE, "/api/fling/{flingId}")
|
||||||
|
.hasAnyAuthority(FLING_ADMIN.getAuthority())
|
||||||
|
.and()
|
||||||
|
.authorizeRequests()
|
||||||
|
.antMatchers(HttpMethod.PUT, "/api/fling/{flingId}")
|
||||||
|
.hasAnyAuthority(FLING_ADMIN.getAuthority())
|
||||||
|
.and()
|
||||||
|
.authorizeRequests()
|
||||||
|
.antMatchers(HttpMethod.POST, "/api/fling")
|
||||||
|
.hasAuthority(FLING_ADMIN.getAuthority())
|
||||||
|
.and()
|
||||||
|
.authorizeRequests()
|
||||||
|
.antMatchers(HttpMethod.GET, "/api/fling")
|
||||||
|
.hasAuthority(FLING_ADMIN.getAuthority())
|
||||||
|
.and()
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************/
|
||||||
|
/** Authorization for: /api/artifacts **/
|
||||||
|
/***************************************/
|
||||||
|
.authorizeRequests()
|
||||||
|
.antMatchers(HttpMethod.GET, "/api/artifacts/{artifactId}/**")
|
||||||
|
.access("@authorizationService.allowArtifactAccess(#artifactId, authentication)")
|
||||||
|
.and()
|
||||||
|
.authorizeRequests()
|
||||||
|
.antMatchers(HttpMethod.POST, "/api/artifacts/{artifactId}/data")
|
||||||
|
.access("@authorizationService.allowArtifactUpload(#artifactId, authentication)")
|
||||||
|
.and()
|
||||||
|
.authorizeRequests()
|
||||||
|
.antMatchers(HttpMethod.DELETE, "/api/artifacts/{artifactId}")
|
||||||
|
.access("@authorizationService.allowArtifactUpload(#artifactId, authentication)");
|
||||||
|
|
||||||
//@formatter:on
|
//@formatter:on
|
||||||
}
|
}
|
||||||
|
|
||||||
private RequestMatcher modificationMethodsAntMatcher(String antPattern) {
|
|
||||||
return multiMethodAntMatcher(antPattern,
|
|
||||||
HttpMethod.PATCH, HttpMethod.PUT,
|
|
||||||
HttpMethod.POST, HttpMethod.DELETE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private RequestMatcher multiMethodAntMatcher(String antPattern, HttpMethod... httpMethods) {
|
|
||||||
List<RequestMatcher> antMatchers = Arrays.stream(httpMethods)
|
|
||||||
.map(m -> new AntPathRequestMatcher(antPattern, m.toString()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
return new OrRequestMatcher(antMatchers);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public CorsConfigurationSource corsConfigurationSource() {
|
public CorsConfigurationSource corsConfigurationSource() {
|
||||||
// see https://stackoverflow.com/a/43559266
|
// see https://stackoverflow.com/a/43559266
|
||||||
|
|
||||||
log.info("Allowed origins: {}", securityConfiguration.getAllowedOrigins());
|
log.info("Allowed origins: {}", allowedOrigins);
|
||||||
|
|
||||||
CorsConfiguration configuration = new CorsConfiguration();
|
CorsConfiguration configuration = new CorsConfiguration();
|
||||||
configuration.setAllowedOrigins(securityConfiguration.getAllowedOrigins());
|
configuration.setAllowedOrigins(allowedOrigins);
|
||||||
configuration.setAllowedMethods(List.of("*"));
|
configuration.setAllowedMethods(List.of("*"));
|
||||||
|
|
||||||
// setAllowCredentials(true) is important, otherwise:
|
// setAllowCredentials(true) is important, otherwise:
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
package net.friedl.fling.security.authentication;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
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 net.friedl.fling.security.authentication.dto.OwnerAuthDto;
|
|
||||||
import net.friedl.fling.security.authentication.dto.UserAuthDto;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api")
|
|
||||||
public class AuthenticationController {
|
|
||||||
|
|
||||||
private AuthenticationService authenticationService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public AuthenticationController(AuthenticationService authenticationService) {
|
|
||||||
this.authenticationService = authenticationService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/auth/owner")
|
|
||||||
public String authenticateOwner(@RequestBody OwnerAuthDto ownerAuthDto) {
|
|
||||||
return authenticationService.authenticate(ownerAuthDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/auth/user")
|
|
||||||
public String authenticateUser(@RequestBody UserAuthDto userAuthDto) {
|
|
||||||
return authenticationService.authenticate(userAuthDto);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,100 +0,0 @@
|
||||||
package net.friedl.fling.security.authentication;
|
|
||||||
|
|
||||||
import java.security.Key;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Date;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.security.access.AccessDeniedException;
|
|
||||||
import org.springframework.security.authentication.BadCredentialsException;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import io.jsonwebtoken.Claims;
|
|
||||||
import io.jsonwebtoken.JwtBuilder;
|
|
||||||
import io.jsonwebtoken.JwtParser;
|
|
||||||
import io.jsonwebtoken.Jwts;
|
|
||||||
import net.friedl.fling.security.FlingAuthority;
|
|
||||||
import net.friedl.fling.security.FlingSecurityConfiguration;
|
|
||||||
import net.friedl.fling.security.authentication.dto.OwnerAuthDto;
|
|
||||||
import net.friedl.fling.security.authentication.dto.UserAuthDto;
|
|
||||||
import net.friedl.fling.service.FlingService;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class AuthenticationService {
|
|
||||||
private FlingService flingService;
|
|
||||||
private JwtParser jwtParser;
|
|
||||||
private Key signingKey;
|
|
||||||
private FlingSecurityConfiguration securityConfig;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public AuthenticationService(JwtParser jwtParser, Key signingKey, FlingService flingService,
|
|
||||||
FlingSecurityConfiguration securityConfig) {
|
|
||||||
this.flingService = flingService;
|
|
||||||
this.jwtParser = jwtParser;
|
|
||||||
this.signingKey = signingKey;
|
|
||||||
this.securityConfig = securityConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String authenticate(OwnerAuthDto ownerAuth) {
|
|
||||||
if (!securityConfig.getAdminUser().equals(ownerAuth.getUsername())) {
|
|
||||||
throw new AccessDeniedException("Wrong credentials");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!securityConfig.getAdminPassword().equals(ownerAuth.getPassword())) {
|
|
||||||
throw new AccessDeniedException("Wrong credentials");
|
|
||||||
}
|
|
||||||
|
|
||||||
return makeBaseBuilder()
|
|
||||||
.setSubject("owner")
|
|
||||||
.compact();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String authenticate(UserAuthDto userAuth) {
|
|
||||||
var fling = flingService.findFlingByShareId(userAuth.getShareId())
|
|
||||||
.orElseThrow();
|
|
||||||
String authCode = userAuth.getCode();
|
|
||||||
|
|
||||||
if (!flingService.hasAuthCode(fling.getId(), authCode)) {
|
|
||||||
throw new AccessDeniedException("Wrong fling code");
|
|
||||||
}
|
|
||||||
|
|
||||||
return makeBaseBuilder()
|
|
||||||
.setSubject("user")
|
|
||||||
.claim("sid", fling.getShareUrl())
|
|
||||||
.compact();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public Authentication parseAuthentication(String token) {
|
|
||||||
Claims claims = parseClaims(token);
|
|
||||||
|
|
||||||
FlingAuthority authority;
|
|
||||||
Long flingId;
|
|
||||||
|
|
||||||
switch (claims.getSubject()) {
|
|
||||||
case "owner":
|
|
||||||
authority = FlingAuthority.FLING_OWNER;
|
|
||||||
flingId = null;
|
|
||||||
break;
|
|
||||||
case "user":
|
|
||||||
authority = FlingAuthority.FLING_USER;
|
|
||||||
var sid = claims.get("sid", String.class);
|
|
||||||
flingId = flingService.findFlingByShareId(sid).orElseThrow().getId();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new BadCredentialsException("Invalid token");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new FlingToken(new GrantedFlingAuthority(authority, flingId));
|
|
||||||
}
|
|
||||||
|
|
||||||
private JwtBuilder makeBaseBuilder() {
|
|
||||||
return Jwts.builder()
|
|
||||||
.setIssuedAt(Date.from(Instant.now()))
|
|
||||||
.setExpiration(Date.from(Instant.now().plusSeconds(securityConfig.getJwtExpiration())))
|
|
||||||
.signWith(signingKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Claims parseClaims(String token) {
|
|
||||||
return jwtParser.parseClaimsJws(token).getBody();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,25 +1,42 @@
|
||||||
package net.friedl.fling.security.authentication;
|
package net.friedl.fling.security.authentication;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import net.friedl.fling.security.authentication.authorities.FlingAdminAuthority;
|
||||||
|
import net.friedl.fling.security.authentication.authorities.FlingUserAuthority;
|
||||||
|
|
||||||
public class FlingToken extends AbstractAuthenticationToken {
|
public class FlingToken extends AbstractAuthenticationToken {
|
||||||
|
|
||||||
private static final long serialVersionUID = -1112423505610346583L;
|
private static final long serialVersionUID = -1112423505610346583L;
|
||||||
private GrantedFlingAuthority grantedFlingAuthority;
|
private String token;
|
||||||
|
|
||||||
public FlingToken(GrantedFlingAuthority authority) {
|
public FlingToken(List<GrantedAuthority> authorities, String token) {
|
||||||
super(List.of(authority));
|
super(authorities);
|
||||||
this.grantedFlingAuthority = authority;
|
this.token = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public GrantedFlingAuthority getGrantedFlingAuthority() {
|
public boolean authorizedForFling(UUID id) {
|
||||||
return this.grantedFlingAuthority;
|
for (GrantedAuthority grantedAuthority : getAuthorities()) {
|
||||||
|
if (grantedAuthority instanceof FlingAdminAuthority) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grantedAuthority instanceof FlingUserAuthority) {
|
||||||
|
UUID grantedFlingId = ((FlingUserAuthority) grantedAuthority).getFlingId();
|
||||||
|
if (grantedFlingId.equals(id)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object getCredentials() {
|
public String getCredentials() {
|
||||||
return null;
|
return this.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
package net.friedl.fling.security.authentication;
|
|
||||||
|
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
|
||||||
import net.friedl.fling.security.FlingAuthority;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Authority granting access to a fling
|
|
||||||
*
|
|
||||||
* @author Armin Friedl <dev@friedl.net>
|
|
||||||
*/
|
|
||||||
public class GrantedFlingAuthority implements GrantedAuthority {
|
|
||||||
|
|
||||||
private static final long serialVersionUID = -1552301479158714777L;
|
|
||||||
|
|
||||||
private FlingAuthority authority;
|
|
||||||
private Long flingId;
|
|
||||||
|
|
||||||
public GrantedFlingAuthority(FlingAuthority authority, Long flingId) {
|
|
||||||
this.authority = authority;
|
|
||||||
this.flingId = flingId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getFlingId() {
|
|
||||||
return this.flingId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getAuthority() {
|
|
||||||
return authority.name();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package net.friedl.fling.security.authentication.authorities;
|
||||||
|
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import net.friedl.fling.security.FlingAuthorities;
|
||||||
|
|
||||||
|
public class FlingAdminAuthority implements GrantedAuthority {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = -4605768612393081070L;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAuthority() {
|
||||||
|
return FlingAuthorities.FLING_ADMIN.getAuthority();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package net.friedl.fling.security.authentication.authorities;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import net.friedl.fling.security.FlingAuthorities;
|
||||||
|
|
||||||
|
public class FlingUserAuthority implements GrantedAuthority {
|
||||||
|
private static final long serialVersionUID = -1814514234042184275L;
|
||||||
|
|
||||||
|
private UUID flingId;
|
||||||
|
|
||||||
|
public FlingUserAuthority(UUID flingId) {
|
||||||
|
this.flingId = flingId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAuthority() {
|
||||||
|
return FlingAuthorities.FLING_USER.getAuthority();
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getFlingId() {
|
||||||
|
return flingId;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,9 +0,0 @@
|
||||||
package net.friedl.fling.security.authentication.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class OwnerAuthDto {
|
|
||||||
private String username;
|
|
||||||
private String password;
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
package net.friedl.fling.security.authentication.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class UserAuthDto {
|
|
||||||
String shareId;
|
|
||||||
String code;
|
|
||||||
}
|
|
|
@ -1,28 +1,31 @@
|
||||||
package net.friedl.fling.security.authentication;
|
package net.friedl.fling.security.authentication.filter;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import javax.servlet.FilterChain;
|
import javax.servlet.FilterChain;
|
||||||
import javax.servlet.ServletException;
|
import javax.servlet.ServletException;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.context.SecurityContext;
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.friedl.fling.security.authentication.FlingToken;
|
||||||
|
import net.friedl.fling.service.AuthenticationService;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
public class BearerAuthenticationFilter extends OncePerRequestFilter {
|
||||||
private static final String TOKEN_PREFIX = "Bearer ";
|
private static final String TOKEN_PREFIX = "Bearer ";
|
||||||
private static final String HEADER_STRING = "Authorization";
|
private static final String HEADER_STRING = "Authorization";
|
||||||
|
|
||||||
private AuthenticationService authenticationService;
|
private AuthenticationService authenticationService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public JwtAuthenticationFilter(AuthenticationService authenticationService) {
|
public BearerAuthenticationFilter(AuthenticationService authenticationService) {
|
||||||
this.authenticationService = authenticationService;
|
this.authenticationService = authenticationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +37,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
String header = request.getHeader(HEADER_STRING);
|
String header = request.getHeader(HEADER_STRING);
|
||||||
|
|
||||||
if (header == null || !header.startsWith(TOKEN_PREFIX)) {
|
if (header == null || !header.startsWith(TOKEN_PREFIX)) {
|
||||||
log.warn("Could not find bearer token. No JWT authentication.");
|
log.info("Anonymous request for {} {}{}", request.getMethod(), request.getRequestURL(),
|
||||||
|
request.getQueryString() != null ? "?" + request.getQueryString() : "");
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -44,8 +48,12 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
SecurityContext securityContext = SecurityContextHolder.getContext();
|
SecurityContext securityContext = SecurityContextHolder.getContext();
|
||||||
|
|
||||||
if (securityContext.getAuthentication() == null) {
|
if (securityContext.getAuthentication() == null) {
|
||||||
Authentication authentication = authenticationService.parseAuthentication(authToken);
|
log.info("Authenticating request for {} {}{}", request.getMethod(), request.getRequestURL(),
|
||||||
securityContext.setAuthentication(authentication);
|
request.getQueryString() != null ? "?" + request.getQueryString() : "");
|
||||||
|
FlingToken token = authenticationService.parseJwtAuthentication(authToken);
|
||||||
|
log.info("Authenticated as {}", token.getAuthorities().stream()
|
||||||
|
.map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")));
|
||||||
|
securityContext.setAuthentication(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
|
@ -0,0 +1,59 @@
|
||||||
|
package net.friedl.fling.security.authentication.filter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.friedl.fling.security.authentication.FlingToken;
|
||||||
|
import net.friedl.fling.service.AuthenticationService;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class TokenAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
private AuthenticationService authenticationService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public TokenAuthenticationFilter(AuthenticationService authenticationService) {
|
||||||
|
this.authenticationService = authenticationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||||
|
FilterChain filterChain) throws ServletException, IOException {
|
||||||
|
|
||||||
|
String derivedToken = request.getParameter("derivedToken");
|
||||||
|
if (derivedToken == null) {
|
||||||
|
log.info("No derived token in request for {} {}{}", request.getMethod(),
|
||||||
|
request.getRequestURL(),
|
||||||
|
request.getQueryString() != null ? "?" + request.getQueryString() : "");
|
||||||
|
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SecurityContext securityContext = SecurityContextHolder.getContext();
|
||||||
|
if (securityContext.getAuthentication() == null) {
|
||||||
|
log.info("Authenticating request for {} {}{}", request.getMethod(), request.getRequestURL(),
|
||||||
|
request.getQueryString() != null ? "?" + request.getQueryString() : "");
|
||||||
|
|
||||||
|
FlingToken token = authenticationService.parseDerivedToken(derivedToken);
|
||||||
|
|
||||||
|
log.info("Authenticated as {}", token.getAuthorities().stream()
|
||||||
|
.map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")));
|
||||||
|
|
||||||
|
securityContext.setAuthentication(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,89 +1,78 @@
|
||||||
package net.friedl.fling.service;
|
package net.friedl.fling.service;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.UUID;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import javax.transaction.Transactional;
|
import javax.transaction.Transactional;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.json.JsonParser;
|
|
||||||
import org.springframework.boot.json.JsonParserFactory;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import net.friedl.fling.model.dto.ArtifactDto;
|
import net.friedl.fling.model.dto.ArtifactDto;
|
||||||
import net.friedl.fling.model.mapper.ArtifactMapper;
|
import net.friedl.fling.model.mapper.ArtifactMapper;
|
||||||
import net.friedl.fling.persistence.archive.Archive;
|
|
||||||
import net.friedl.fling.persistence.archive.ArchiveException;
|
|
||||||
import net.friedl.fling.persistence.entities.ArtifactEntity;
|
import net.friedl.fling.persistence.entities.ArtifactEntity;
|
||||||
|
import net.friedl.fling.persistence.entities.FlingEntity;
|
||||||
import net.friedl.fling.persistence.repositories.ArtifactRepository;
|
import net.friedl.fling.persistence.repositories.ArtifactRepository;
|
||||||
import net.friedl.fling.persistence.repositories.FlingRepository;
|
import net.friedl.fling.persistence.repositories.FlingRepository;
|
||||||
|
import net.friedl.fling.service.archive.ArchiveService;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@Transactional
|
@Transactional
|
||||||
public class ArtifactService {
|
public class ArtifactService {
|
||||||
|
|
||||||
private FlingRepository flingRepository;
|
|
||||||
private ArtifactRepository artifactRepository;
|
private ArtifactRepository artifactRepository;
|
||||||
|
private FlingRepository flingRepository;
|
||||||
private ArtifactMapper artifactMapper;
|
private ArtifactMapper artifactMapper;
|
||||||
private Archive archive;
|
private ArchiveService archiveService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public ArtifactService(ArtifactRepository artifactRepository, FlingRepository flingRepository,
|
public ArtifactService(ArtifactRepository artifactRepository, FlingRepository flingRepository,
|
||||||
ArtifactMapper artifactMapper, Archive archive) {
|
ArtifactMapper artifactMapper, ArchiveService archiveService) {
|
||||||
|
|
||||||
this.artifactRepository = artifactRepository;
|
this.artifactRepository = artifactRepository;
|
||||||
this.flingRepository = flingRepository;
|
this.flingRepository = flingRepository;
|
||||||
this.artifactMapper = artifactMapper;
|
this.artifactMapper = artifactMapper;
|
||||||
this.archive = archive;
|
this.archiveService = archiveService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ArtifactDto> findAllArtifacts(Long flingId) {
|
/**
|
||||||
return artifactMapper.map(artifactRepository.findAllByFlingId(flingId));
|
* Fetch an {@link ArtifactDto} by id. Must be called with a valid artifact id, otherwise bails
|
||||||
|
* out with a {@link RuntimeException}. Synchronization must be done on client side.
|
||||||
|
*
|
||||||
|
* @param id A valid {@link UUID} for an existing entity in the database. Not null.
|
||||||
|
* @return The ArtifactDto corresponding to the {@code id}
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
public ArtifactDto getById(@NotNull UUID id) {
|
||||||
|
return artifactMapper.map(artifactRepository.getOne(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ArtifactDto storeArtifact(Long flingId, InputStream artifact) throws ArchiveException {
|
/**
|
||||||
var flingEntity = flingRepository.findById(flingId).orElseThrow();
|
* Create a new {@link ArtifactEntity} from {@code artifactDto} for the fling {@code flingId}.
|
||||||
var archiveId = archive.store(artifact);
|
*
|
||||||
|
* @param flingId Id of an existing {@link FlingEntity}
|
||||||
|
* @param artifactDto The data for the new {@link ArtifactEntity}
|
||||||
|
* @return The newly created artifact
|
||||||
|
*/
|
||||||
|
public ArtifactDto create(UUID flingId, ArtifactDto artifactDto) {
|
||||||
|
FlingEntity flingEntity = flingRepository.getOne(flingId);
|
||||||
|
|
||||||
ArtifactEntity artifactEntity = new ArtifactEntity();
|
log.debug("Creating new ArtifactEntity for ArtifactDto[.path={}]", artifactDto.getPath());
|
||||||
artifactEntity.setDoi(archiveId);
|
ArtifactEntity artifactEntity = artifactMapper.map(artifactDto);
|
||||||
artifactEntity.setFling(flingEntity);
|
artifactEntity.setFling(flingEntity);
|
||||||
|
artifactEntity = artifactRepository.save(artifactEntity);
|
||||||
artifactRepository.save(artifactEntity);
|
|
||||||
|
|
||||||
return artifactMapper.map(artifactEntity);
|
return artifactMapper.map(artifactEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<ArtifactDto> findArtifact(Long artifactId) {
|
/**
|
||||||
return artifactMapper.map(artifactRepository.findById(artifactId));
|
* Deletes an artifact identified by {@code id}. NOOP if the artifact cannot be found.
|
||||||
}
|
*
|
||||||
|
* @param id An {@link UUID} that identifies the artifact
|
||||||
public ArtifactDto mergeArtifact(Long artifactId, String body) {
|
* @throws IOException If the deletion failed
|
||||||
JsonParser jsonParser = JsonParserFactory.getJsonParser();
|
*/
|
||||||
Map<String, Object> parsedBody = jsonParser.parseMap(body);
|
public void delete(UUID id) throws IOException {
|
||||||
|
archiveService.deleteArtifact(id);
|
||||||
artifactRepository.findById(artifactId)
|
artifactRepository.deleteById(id);
|
||||||
// map entity to dto
|
log.info("Deleted artifact {}", id);
|
||||||
.map(artifactMapper::map)
|
|
||||||
// merge parsedBody into dto
|
|
||||||
.map(a -> artifactMapper.merge(a, parsedBody))
|
|
||||||
// map dto to entity
|
|
||||||
.map(artifactMapper::map)
|
|
||||||
.ifPresent(artifactRepository::save);
|
|
||||||
|
|
||||||
return artifactMapper.map(artifactRepository.getOne(artifactId));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteArtifact(Long artifactId) throws ArchiveException {
|
|
||||||
var doi = artifactRepository.getOne(artifactId).getDoi();
|
|
||||||
artifactRepository.deleteById(artifactId);
|
|
||||||
archive.remove(doi);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String generateDownloadId(Long artifactId) {
|
|
||||||
// TODO: This id is not secured! Generate temporary download id
|
|
||||||
return artifactRepository.getOne(artifactId).getDoi();
|
|
||||||
}
|
|
||||||
|
|
||||||
public InputStream downloadArtifact(String downloadId) throws ArchiveException {
|
|
||||||
return archive.get(downloadId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,179 @@
|
||||||
|
package net.friedl.fling.service;
|
||||||
|
|
||||||
|
import java.security.Key;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import javax.persistence.EntityNotFoundException;
|
||||||
|
import javax.transaction.Transactional;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import io.jsonwebtoken.JwtBuilder;
|
||||||
|
import io.jsonwebtoken.JwtParser;
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.friedl.fling.model.dto.AdminAuthDto;
|
||||||
|
import net.friedl.fling.model.dto.UserAuthDto;
|
||||||
|
import net.friedl.fling.persistence.entities.FlingEntity;
|
||||||
|
import net.friedl.fling.persistence.entities.TokenEntity;
|
||||||
|
import net.friedl.fling.persistence.repositories.FlingRepository;
|
||||||
|
import net.friedl.fling.persistence.repositories.TokenRepository;
|
||||||
|
import net.friedl.fling.security.authentication.FlingToken;
|
||||||
|
import net.friedl.fling.security.authentication.authorities.FlingAdminAuthority;
|
||||||
|
import net.friedl.fling.security.authentication.authorities.FlingUserAuthority;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class AuthenticationService {
|
||||||
|
private JwtParser jwtParser;
|
||||||
|
private Key jwtSigningKey;
|
||||||
|
private FlingRepository flingRepository;
|
||||||
|
private TokenRepository tokenRepository;
|
||||||
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
@Value("${fling.security.admin-name}")
|
||||||
|
private String adminName;
|
||||||
|
@Value("${fling.security.admin-password}")
|
||||||
|
private String adminPassword;
|
||||||
|
@Value("${fling.security.jwt-expiration}")
|
||||||
|
private Long jwtExpiration;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public AuthenticationService(JwtParser jwtParser, Key jwtSigningKey,
|
||||||
|
PasswordEncoder passwordEncoder, FlingRepository flingRepository,
|
||||||
|
TokenRepository tokenRepository) {
|
||||||
|
|
||||||
|
this.jwtParser = jwtParser;
|
||||||
|
this.jwtSigningKey = jwtSigningKey;
|
||||||
|
this.passwordEncoder = passwordEncoder;
|
||||||
|
this.flingRepository = flingRepository;
|
||||||
|
this.tokenRepository = tokenRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> authenticate(AdminAuthDto adminAuth) {
|
||||||
|
log.info("Authenticating {}", adminAuth.getAdminName());
|
||||||
|
if (!adminName.equals(adminAuth.getAdminName())) {
|
||||||
|
log.debug("Authentication failed for {}", adminAuth.getAdminName());
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!adminPassword.equals(adminAuth.getAdminPassword())) {
|
||||||
|
log.debug("Authentication failed for {}", adminAuth.getAdminName());
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Authentication successful for {}", adminAuth.getAdminName());
|
||||||
|
return Optional.of(
|
||||||
|
getJwtBuilder()
|
||||||
|
.setSubject("admin")
|
||||||
|
.compact());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> authenticate(UserAuthDto userAuth) {
|
||||||
|
log.info("Authenticating for fling [.shareId={}]", userAuth.getShareId());
|
||||||
|
FlingEntity flingEntity = flingRepository.findByShareId(userAuth.getShareId());
|
||||||
|
if (flingEntity == null) {
|
||||||
|
throw new EntityNotFoundException("No entity for shareId=" + userAuth.getShareId());
|
||||||
|
}
|
||||||
|
|
||||||
|
String providedAuthCode = userAuth.getAuthCode();
|
||||||
|
String actualAuthCodeHash = flingEntity.getAuthCode();
|
||||||
|
|
||||||
|
Boolean isProtected = StringUtils.hasText(actualAuthCodeHash);
|
||||||
|
|
||||||
|
if(!isProtected) log.debug("No protection set for fling [.shareId={}]");
|
||||||
|
|
||||||
|
if (isProtected && !passwordEncoder.matches(providedAuthCode, actualAuthCodeHash)) {
|
||||||
|
log.debug("Authentication failed for fling [.shareId={}]", userAuth.getShareId());
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Authentication successful for fling [.shareId={}]", userAuth.getShareId());
|
||||||
|
return Optional.of(
|
||||||
|
getJwtBuilder()
|
||||||
|
.setSubject("user")
|
||||||
|
.claim("shareId", flingEntity.getShareId())
|
||||||
|
.claim("id", flingEntity.getId())
|
||||||
|
.compact());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public FlingToken parseJwtAuthentication(String token) {
|
||||||
|
Claims claims = jwtParser.parseClaimsJws(token).getBody();
|
||||||
|
|
||||||
|
switch (claims.getSubject()) {
|
||||||
|
case "admin":
|
||||||
|
return new FlingToken(List.of(new FlingAdminAuthority()), token);
|
||||||
|
case "user":
|
||||||
|
UUID grantedFlingId = UUID.fromString(claims.get("id", String.class));
|
||||||
|
return new FlingToken(List.of(new FlingUserAuthority(grantedFlingId)), token);
|
||||||
|
default:
|
||||||
|
throw new BadCredentialsException("Invalid token");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new JwtBuilder. A new builder must be constructed for each JWT creation, because the
|
||||||
|
* builder keeps its state.
|
||||||
|
*
|
||||||
|
* @return A new JwtBuilder with basic default configuration.
|
||||||
|
*/
|
||||||
|
private JwtBuilder getJwtBuilder() {
|
||||||
|
return Jwts.builder()
|
||||||
|
.setIssuedAt(Date.from(Instant.now()))
|
||||||
|
.setExpiration(Date.from(Instant.now().plusSeconds(jwtExpiration)))
|
||||||
|
.signWith(jwtSigningKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a derived token with the given settings. Note that the returned string is opaque and
|
||||||
|
* should not not be interpreted in any way but only used as is.
|
||||||
|
*
|
||||||
|
* @param singleUse Whether this token should be deleted after a single use
|
||||||
|
* @return An opaque string representing the token
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public String deriveToken(Boolean singleUse) {
|
||||||
|
UUID id = UUID.randomUUID();
|
||||||
|
TokenEntity tokenEntity = new TokenEntity();
|
||||||
|
tokenEntity.setId(id);
|
||||||
|
if (singleUse != null) {
|
||||||
|
tokenEntity.setSingleUse(singleUse);
|
||||||
|
}
|
||||||
|
|
||||||
|
SecurityContext securityContext = SecurityContextHolder.getContext();
|
||||||
|
if (securityContext.getAuthentication() instanceof FlingToken) {
|
||||||
|
FlingToken flingToken = (FlingToken) securityContext.getAuthentication();
|
||||||
|
tokenEntity.setToken(flingToken.getCredentials());
|
||||||
|
} else {
|
||||||
|
// This should be prevented in FlingWebSecurityConfigurer
|
||||||
|
throw new IllegalStateException("Cannot derive token from current authentication");
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenRepository.save(tokenEntity);
|
||||||
|
|
||||||
|
return id.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public FlingToken parseDerivedToken(String derivedToken) {
|
||||||
|
TokenEntity tokenEntity = tokenRepository.getOne(UUID.fromString(derivedToken));
|
||||||
|
|
||||||
|
FlingToken flingToken = parseJwtAuthentication(tokenEntity.getToken());
|
||||||
|
|
||||||
|
if (tokenEntity.getSingleUse()) {
|
||||||
|
tokenRepository.delete(tokenEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return flingToken;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
package net.friedl.fling.service;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
import javax.persistence.EntityNotFoundException;
|
||||||
|
import javax.transaction.Transactional;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.friedl.fling.persistence.entities.FlingEntity;
|
||||||
|
import net.friedl.fling.persistence.repositories.FlingRepository;
|
||||||
|
import net.friedl.fling.security.FlingAuthorities;
|
||||||
|
import net.friedl.fling.security.authentication.FlingToken;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
public class AuthorizationService {
|
||||||
|
private FlingRepository flingRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public AuthorizationService(FlingRepository flingRepository) {
|
||||||
|
this.flingRepository = flingRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean allowUpload(UUID flingId, AbstractAuthenticationToken token) {
|
||||||
|
if (!(token instanceof FlingToken)) {
|
||||||
|
log.debug("Token of type {} not allowed. Authentication denied.", token.getClass());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FlingAuthorities.FLING_ADMIN.verify(token)) {
|
||||||
|
log.debug("Owner authorized for upload fling[.id={}]", flingId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlingEntity flingEntity = flingRepository.getOne(flingId);
|
||||||
|
if (flingEntity.getAllowUpload() == null || !flingEntity.getAllowUpload()) {
|
||||||
|
log.debug("Fling[.id={}] does not not allow uploads");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlingToken flingToken = (FlingToken) token;
|
||||||
|
if (flingToken.authorizedForFling(flingId)) {
|
||||||
|
log.debug("User authorized for upload fling[.id={}]", flingId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("User not authorized for upload fling[.id={}]", flingId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean allowFlingAccess(UUID flingId, AbstractAuthenticationToken token) {
|
||||||
|
if (!(token instanceof FlingToken)) {
|
||||||
|
log.debug("Token of type {} not allowed. Authentication denied.", token.getClass());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FlingAuthorities.FLING_ADMIN.verify(token)) {
|
||||||
|
log.debug("Owner authorized for fling access [id = {}]", flingId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlingToken flingToken = (FlingToken) token;
|
||||||
|
if (flingToken.authorizedForFling(flingId)) {
|
||||||
|
log.debug("User authorized for fling access [id = {}]");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("User not authorized to access fling[.id={}]", flingId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean allowFlingAccessByShareId(String shareId, AbstractAuthenticationToken token) {
|
||||||
|
if (FlingAuthorities.FLING_ADMIN.verify(token)) {
|
||||||
|
log.debug("Owner authorized for fling access [shareId = {}]", shareId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlingEntity flingEntity = flingRepository.findByShareId(shareId);
|
||||||
|
if (flingEntity == null) {
|
||||||
|
throw new EntityNotFoundException("No entity for shareId=" + shareId);
|
||||||
|
}
|
||||||
|
return allowFlingAccess(flingEntity.getId(), token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean allowArtifactAccess(UUID artifactId, AbstractAuthenticationToken token) {
|
||||||
|
FlingEntity flingEntity = flingRepository.findByArtifactId(artifactId);
|
||||||
|
return allowFlingAccess(flingEntity.getId(), token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean allowArtifactUpload(UUID artifactId, AbstractAuthenticationToken token) {
|
||||||
|
FlingEntity flingEntity = flingRepository.findByArtifactId(artifactId);
|
||||||
|
return allowUpload(flingEntity.getId(), token);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,36 +1,26 @@
|
||||||
package net.friedl.fling.service;
|
package net.friedl.fling.service;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.UUID;
|
||||||
import java.util.function.Supplier;
|
import javax.persistence.EntityNotFoundException;
|
||||||
import java.util.zip.Deflater;
|
|
||||||
import java.util.zip.ZipEntry;
|
|
||||||
import java.util.zip.ZipOutputStream;
|
|
||||||
import javax.transaction.Transactional;
|
import javax.transaction.Transactional;
|
||||||
|
import javax.validation.Valid;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.util.Pair;
|
|
||||||
import org.springframework.security.crypto.codec.Hex;
|
|
||||||
import org.springframework.security.crypto.keygen.KeyGenerators;
|
import org.springframework.security.crypto.keygen.KeyGenerators;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.friedl.fling.model.dto.ArtifactDto;
|
||||||
import net.friedl.fling.model.dto.FlingDto;
|
import net.friedl.fling.model.dto.FlingDto;
|
||||||
|
import net.friedl.fling.model.mapper.ArtifactMapper;
|
||||||
import net.friedl.fling.model.mapper.FlingMapper;
|
import net.friedl.fling.model.mapper.FlingMapper;
|
||||||
import net.friedl.fling.persistence.archive.Archive;
|
|
||||||
import net.friedl.fling.persistence.archive.ArchiveException;
|
|
||||||
import net.friedl.fling.persistence.entities.ArtifactEntity;
|
|
||||||
import net.friedl.fling.persistence.entities.FlingEntity;
|
import net.friedl.fling.persistence.entities.FlingEntity;
|
||||||
import net.friedl.fling.persistence.repositories.FlingRepository;
|
import net.friedl.fling.persistence.repositories.FlingRepository;
|
||||||
|
import net.friedl.fling.service.archive.ArchiveService;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
|
@ -39,135 +29,115 @@ public class FlingService {
|
||||||
|
|
||||||
private FlingRepository flingRepository;
|
private FlingRepository flingRepository;
|
||||||
private FlingMapper flingMapper;
|
private FlingMapper flingMapper;
|
||||||
private Archive archive;
|
private ArtifactMapper artifactMapper;
|
||||||
private MessageDigest keyHashDigest;
|
private ArchiveService archiveService;
|
||||||
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public FlingService(FlingRepository flingRepository, FlingMapper flingMapper, Archive archive,
|
public FlingService(FlingRepository flingRepository, FlingMapper flingMapper,
|
||||||
MessageDigest keyHashDigest) {
|
ArtifactMapper artifactMapper,
|
||||||
|
ArchiveService archiveService, PasswordEncoder passwordEcoder) {
|
||||||
|
|
||||||
this.flingRepository = flingRepository;
|
this.flingRepository = flingRepository;
|
||||||
this.flingMapper = flingMapper;
|
this.flingMapper = flingMapper;
|
||||||
this.archive = archive;
|
this.artifactMapper = artifactMapper;
|
||||||
this.keyHashDigest = keyHashDigest;
|
this.archiveService = archiveService;
|
||||||
|
this.passwordEncoder = passwordEcoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a list of all flings
|
||||||
|
*
|
||||||
|
* @return A list of all flings
|
||||||
|
*/
|
||||||
public List<FlingDto> findAll() {
|
public List<FlingDto> findAll() {
|
||||||
return flingMapper.map(flingRepository.findAll());
|
return flingMapper.mapEntities(flingRepository.findAll());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long createFling(FlingDto flingDto) {
|
/**
|
||||||
if (!StringUtils.hasText(flingDto.getShareUrl())) {
|
* Get a fling by id
|
||||||
flingDto.setShareUrl(generateShareUrl());
|
*
|
||||||
|
* @param id Id of the fling. Must exist.
|
||||||
|
* @return The fling
|
||||||
|
*/
|
||||||
|
public FlingDto getById(UUID id) {
|
||||||
|
return flingMapper.map(flingRepository.getOne(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new fling entity from {@code flingDto}
|
||||||
|
*
|
||||||
|
* @param flingDto Base data from which the new fling should be created
|
||||||
|
* @return The created fling
|
||||||
|
*/
|
||||||
|
public FlingDto create(FlingDto flingDto) {
|
||||||
|
log.debug("Creating new fling");
|
||||||
|
FlingEntity flingEntity = flingMapper.map(flingDto);
|
||||||
|
|
||||||
|
if (!StringUtils.hasText(flingEntity.getShareId())) {
|
||||||
|
log.debug("No share id set. Generating random share id");
|
||||||
|
flingEntity.setShareId(generateShareId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.hasText(flingEntity.getAuthCode())) {
|
||||||
|
log.debug("Hashing authentication code for {}", flingEntity.getId());
|
||||||
|
flingEntity.setAuthCode(hashAuthCode(flingDto.getAuthCode()));
|
||||||
}
|
}
|
||||||
|
|
||||||
var flingEntity = flingMapper.map(flingDto);
|
|
||||||
flingEntity.setAuthCode(hashKey(flingEntity.getAuthCode()));
|
|
||||||
flingEntity = flingRepository.save(flingEntity);
|
flingEntity = flingRepository.save(flingEntity);
|
||||||
return flingEntity.getId();
|
log.debug("Created new fling {}", flingEntity.getId());
|
||||||
|
return flingMapper.map(flingEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean existsShareUrl(String shareUrl) {
|
public FlingDto getByShareId(String shareId) {
|
||||||
return !flingRepository.findByShareUrl(shareUrl).isEmpty();
|
FlingEntity flingEntity = flingRepository.findByShareId(shareId);
|
||||||
|
if (flingEntity == null) {
|
||||||
|
throw new EntityNotFoundException("No entity for shareId=" + shareId);
|
||||||
|
}
|
||||||
|
return flingMapper.map(flingEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void mergeFling(Long flingId, FlingDto flingDto) {
|
public void delete(UUID id) throws IOException {
|
||||||
var flingEntity = flingRepository.getOne(flingId);
|
archiveService.deleteFling(id);
|
||||||
|
flingRepository.deleteById(id);
|
||||||
mergeNonEmpty(flingDto::getAllowUpload, flingEntity::setAllowUpload);
|
log.debug("Deleted fling {}", id);
|
||||||
mergeNonEmpty(flingDto::getDirectDownload, flingEntity::setDirectDownload);
|
|
||||||
mergeWithEmpty(flingDto::getExpirationClicks, flingEntity::setExpirationClicks);
|
|
||||||
mergeWithEmpty(flingDto::getExpirationTime, flingEntity::setExpirationTime);
|
|
||||||
mergeNonEmpty(flingDto::getName, flingEntity::setName);
|
|
||||||
mergeNonEmpty(flingDto::getShared, flingEntity::setShared);
|
|
||||||
mergeNonEmpty(flingDto::getShareUrl, flingEntity::setShareUrl);
|
|
||||||
mergeWithEmpty(() -> hashKey(flingDto.getAuthCode()), flingEntity::setAuthCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<FlingDto> findFlingById(Long flingId) {
|
public Set<ArtifactDto> getArtifacts(UUID id) {
|
||||||
return flingMapper.map(flingRepository.findById(flingId));
|
FlingEntity flingEntity = flingRepository.getOne(id);
|
||||||
|
Set<ArtifactDto> artifactDto = artifactMapper.mapEntities(flingEntity.getArtifacts());
|
||||||
|
return artifactDto == null ? Set.of() : artifactDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<FlingDto> findFlingByShareId(String shareUrl) {
|
public boolean validateAuthCode(UUID id, String authCode) {
|
||||||
return flingMapper.map(flingRepository.findByShareUrl(shareUrl));
|
FlingEntity flingEntity = flingRepository.getOne(id);
|
||||||
|
if (StringUtils.hasText(flingEntity.getAuthCode()) != StringUtils.hasText(authCode)) {
|
||||||
|
return false; // only one of them is empty; implicit null safety check
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteFlingById(Long flingId) {
|
boolean valid = flingEntity.getAuthCode().equals(hashAuthCode(authCode));
|
||||||
flingRepository.deleteById(flingId);
|
log.debug("Provided authentication for {} is {} valid", id, valid ? "" : "not");
|
||||||
|
return valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasAuthCode(Long flingId, String authCode) {
|
private String hashAuthCode(String authCode) {
|
||||||
var fling = flingRepository.getOne(flingId);
|
if (!StringUtils.hasText(authCode)) return null;
|
||||||
|
String hash = passwordEncoder.encode(authCode);
|
||||||
if (!StringUtils.hasText(fling.getAuthCode()))
|
log.debug("Hashed authentication code to {}", hash);
|
||||||
return true;
|
return hash;
|
||||||
|
|
||||||
return fling.getAuthCode().equals(hashKey(authCode));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getShareName(String shareUrl) {
|
/**
|
||||||
|
* Generates a URL safe share id
|
||||||
FlingEntity flingEntity = flingRepository.findByShareUrl(shareUrl).orElseThrow();
|
*
|
||||||
|
* @return A random URL safe share id
|
||||||
if (flingEntity.getArtifacts().size() > 1)
|
*/
|
||||||
return flingEntity.getName();
|
private String generateShareId() {
|
||||||
else if (flingEntity.getArtifacts().size() == 1)
|
byte[] key = KeyGenerators
|
||||||
return flingEntity.getArtifacts().stream().findFirst().get().getName();
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long countArtifacts(Long flingId) {
|
|
||||||
return flingRepository.countArtifactsById(flingId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getFlingSize(Long flingId) {
|
|
||||||
var fling = flingRepository.getOne(flingId);
|
|
||||||
|
|
||||||
return fling.getArtifacts().stream()
|
|
||||||
.map(ae -> ae.getSize())
|
|
||||||
.reduce(0L, (acc, as) -> acc + as);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String packageFling(Long flingId) throws IOException, ArchiveException {
|
|
||||||
var fling = flingRepository.getOne(flingId);
|
|
||||||
var tempFile = Files.createTempFile(Long.toString(flingId), ".zip");
|
|
||||||
|
|
||||||
try (var zipStream = new ZipOutputStream(new FileOutputStream(tempFile.toFile()))) {
|
|
||||||
zipStream.setLevel(Deflater.BEST_SPEED);
|
|
||||||
for (ArtifactEntity artifactEntity : fling.getArtifacts()) {
|
|
||||||
ZipEntry ze = new ZipEntry(artifactEntity.getName());
|
|
||||||
zipStream.putNextEntry(ze);
|
|
||||||
|
|
||||||
var artifactStream = archive.get(artifactEntity.getDoi());
|
|
||||||
try (var archiveEntryStream = new BufferedInputStream(artifactStream)) {
|
|
||||||
int b;
|
|
||||||
while ((b = archiveEntryStream.read()) != -1) {
|
|
||||||
zipStream.write(b);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
zipStream.closeEntry();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tempFile.getFileName().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Pair<InputStream, Long> downloadFling(String fileId) throws IOException, ArchiveException {
|
|
||||||
var tempFile = Paths.get(System.getProperty("java.io.tmpdir"), fileId).toFile();
|
|
||||||
|
|
||||||
var archiveLength = tempFile.length();
|
|
||||||
var archiveStream = new FileInputStream(tempFile);
|
|
||||||
|
|
||||||
return Pair.of(archiveStream, archiveLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String generateShareUrl() {
|
|
||||||
var key = KeyGenerators
|
|
||||||
.secureRandom(16)
|
.secureRandom(16)
|
||||||
.generateKey();
|
.generateKey();
|
||||||
|
|
||||||
return Base64.getUrlEncoder().encodeToString(key)
|
String shareId = Base64.getUrlEncoder().encodeToString(key)
|
||||||
// replace all special chars [=-_] in RFC 4648
|
// replace all special chars [=-_] in RFC 4648
|
||||||
// "URL and Filename safe" table with characters from
|
// "URL and Filename safe" table with characters from
|
||||||
// [A-Za-z0-9]. Hence, the generated share url will only consist
|
// [A-Za-z0-9]. Hence, the generated share url will only consist
|
||||||
|
@ -175,23 +145,24 @@ public class FlingService {
|
||||||
.replace('=', 'q')
|
.replace('=', 'q')
|
||||||
.replace('_', 'u')
|
.replace('_', 'u')
|
||||||
.replace('-', 'd');
|
.replace('-', 'd');
|
||||||
|
|
||||||
|
log.debug("Generated share id {}", shareId);
|
||||||
|
return shareId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String hashKey(String key) {
|
public FlingDto replace(UUID id, @Valid FlingDto flingDto) {
|
||||||
if (!StringUtils.hasText(key))
|
FlingEntity flingEntity = flingRepository.getOne(id);
|
||||||
return null;
|
flingEntity.setId(id);
|
||||||
|
flingEntity.setAllowUpload(flingDto.getAllowUpload());
|
||||||
return new String(Hex.encode(keyHashDigest.digest(key.getBytes())));
|
flingEntity.setDirectDownload(flingDto.getDirectDownload());
|
||||||
|
flingEntity.setShared(flingDto.getShared());
|
||||||
|
flingEntity.setExpirationClicks(flingDto.getExpirationClicks());
|
||||||
|
flingEntity.setExpirationTime(flingDto.getExpirationTime());
|
||||||
|
flingEntity.setName(flingDto.getName());
|
||||||
|
flingEntity.setShareId(flingDto.getShareId());
|
||||||
|
if (!flingDto.getAuthCode().equals(flingEntity.getAuthCode())) {
|
||||||
|
flingEntity.setAuthCode(hashAuthCode(flingDto.getAuthCode()));
|
||||||
}
|
}
|
||||||
|
return flingMapper.map(flingEntity);
|
||||||
private <T> void mergeNonEmpty(Supplier<T> sup, Consumer<T> con) {
|
|
||||||
T r = sup.get();
|
|
||||||
if (r != null)
|
|
||||||
con.accept(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
private <T> void mergeWithEmpty(Supplier<T> sup, Consumer<T> con) {
|
|
||||||
T r = sup.get();
|
|
||||||
con.accept(r);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
package net.friedl.fling.service.archive;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for persisting artifacts
|
||||||
|
*
|
||||||
|
* @author Armin Friedl <dev@friedl.net>
|
||||||
|
*/
|
||||||
|
public interface ArchiveService {
|
||||||
|
/**
|
||||||
|
* Retrieve an artifact from the archive
|
||||||
|
*
|
||||||
|
* @param id The artifact id
|
||||||
|
* @return An {@link InputStream} for reading the artifact
|
||||||
|
*/
|
||||||
|
InputStream getArtifact(UUID artifactId) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a packaged fling from the archive
|
||||||
|
*
|
||||||
|
* @param flingId The fling id
|
||||||
|
* @return An {@link InputStream} representing the fling and its artifacts
|
||||||
|
*/
|
||||||
|
InputStream getFling(UUID flingId) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store an artifact
|
||||||
|
*
|
||||||
|
* @param artifactStream The artifact to store represented as {@link InputStream}
|
||||||
|
* @param artifactId The id of the artifact. Must be an existing artifact in the DB. Not null.
|
||||||
|
*/
|
||||||
|
void storeArtifact(UUID artifactId, InputStream artifactStream) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an artifact
|
||||||
|
*
|
||||||
|
* @param id The unique artifact id
|
||||||
|
*/
|
||||||
|
void deleteArtifact(UUID artifactId) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a fling
|
||||||
|
*
|
||||||
|
* @param flingId The unique fling id
|
||||||
|
*/
|
||||||
|
void deleteFling(UUID flingId) throws IOException;
|
||||||
|
}
|
|
@ -0,0 +1,223 @@
|
||||||
|
package net.friedl.fling.service.archive.impl;
|
||||||
|
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.file.FileSystem;
|
||||||
|
import java.nio.file.FileSystems;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import javax.annotation.PreDestroy;
|
||||||
|
import javax.transaction.Transactional;
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.friedl.fling.persistence.entities.ArtifactEntity;
|
||||||
|
import net.friedl.fling.persistence.repositories.ArtifactRepository;
|
||||||
|
import net.friedl.fling.service.archive.ArchiveService;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@ConfigurationProperties("fling.archive.filesystem")
|
||||||
|
@Transactional
|
||||||
|
public class FileSystemArchive implements ArchiveService {
|
||||||
|
@NotBlank
|
||||||
|
private Path archivePath;
|
||||||
|
|
||||||
|
private ArtifactRepository artifactRepository;
|
||||||
|
|
||||||
|
private Map<URI, FileSystem> filesystems;
|
||||||
|
|
||||||
|
public FileSystemArchive(ArtifactRepository artifactRepository) {
|
||||||
|
this.artifactRepository = artifactRepository;
|
||||||
|
this.filesystems = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void postConstruct() {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(archivePath);
|
||||||
|
log.debug("Using archive path {}", archivePath);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Could not create directory at archive path {}", archivePath);
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void preDestroy() {
|
||||||
|
filesystems.forEach((uri, zfs) -> {
|
||||||
|
try {
|
||||||
|
zfs.close();
|
||||||
|
log.debug("Closed {}", uri);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Could not close file system for {}", uri);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getArtifact(UUID artifactId) throws IOException {
|
||||||
|
log.debug("Reading data for artifact {}", artifactId);
|
||||||
|
|
||||||
|
FileSystem zipDisk = getZipDisk(artifactId);
|
||||||
|
return zipDisk.provider().newInputStream(getZipDiskPath(artifactId, zipDisk),
|
||||||
|
StandardOpenOption.READ);
|
||||||
|
|
||||||
|
// do not close zip disk here or the input stream will be closed as well
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getFling(UUID flingId) throws IOException {
|
||||||
|
log.debug("Reading data for fling {}", flingId);
|
||||||
|
Path zipDiskPath = archivePath.resolve(flingId.toString() + ".zip");
|
||||||
|
log.debug("Zip disk path is {}", zipDiskPath);
|
||||||
|
return new FileInputStream(zipDiskPath.toFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void storeArtifact(UUID artifactId, InputStream artifactStream) throws IOException {
|
||||||
|
log.debug("Storing artifact {}", artifactId);
|
||||||
|
|
||||||
|
synchronized (filesystems) {
|
||||||
|
setArchived(artifactId, false);
|
||||||
|
FileSystem zipDisk = getZipDisk(artifactId);
|
||||||
|
Files.copy(artifactStream, getZipDiskPath(artifactId, zipDisk),
|
||||||
|
StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
|
||||||
|
// we need to close the zipDisk in order to flush it to disk
|
||||||
|
closeZipDisk(artifactId);
|
||||||
|
setArchived(artifactId, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteArtifact(UUID artifactId) throws IOException {
|
||||||
|
log.debug("Deleting artifact {}", artifactId);
|
||||||
|
FileSystem zipDisk = getZipDisk(artifactId);
|
||||||
|
Files.delete(getZipDiskPath(artifactId, zipDisk));
|
||||||
|
|
||||||
|
// we need to close the zipDisk in order to flush it to disk
|
||||||
|
closeZipDisk(artifactId);
|
||||||
|
setArchived(artifactId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteFling(UUID flingId) throws IOException {
|
||||||
|
URI zipDiskUri = resolveFlingUri(flingId);
|
||||||
|
|
||||||
|
log.debug("Closing zip disk at {}", zipDiskUri);
|
||||||
|
|
||||||
|
// make sure nobody opens the filesystem while it is being closed and deleted
|
||||||
|
synchronized (filesystems) {
|
||||||
|
FileSystem zipDisk = filesystems.remove(zipDiskUri);
|
||||||
|
|
||||||
|
if (zipDisk != null) {
|
||||||
|
zipDisk.close();
|
||||||
|
log.debug("Zip disk closed");
|
||||||
|
} else {
|
||||||
|
log.debug("No open zip disk found");
|
||||||
|
}
|
||||||
|
|
||||||
|
Path zipDiskPath = archivePath.resolve(flingId.toString() + ".zip");
|
||||||
|
log.debug("Deleting fling [.id={}] at {}", flingId, zipDiskPath);
|
||||||
|
if (Files.exists(zipDiskPath)) {
|
||||||
|
Files.delete(zipDiskPath);
|
||||||
|
} else {
|
||||||
|
log.warn("No fling disk found at {}", zipDiskPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
artifactRepository.findAllByFlingId(flingId).forEach(ar -> ar.setArchived(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setArchived(UUID artifactId, boolean archived) {
|
||||||
|
ArtifactEntity artifactEntity = artifactRepository.getOne(artifactId);
|
||||||
|
artifactEntity.setArchived(archived);
|
||||||
|
log.debug("Artifact[.id={}] set to {} archived", artifactId, archived ? "" : "not");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path getZipDiskPath(UUID artifactId, FileSystem zipDisk) {
|
||||||
|
ArtifactEntity artifactEntity = artifactRepository.getOne(artifactId);
|
||||||
|
log.debug("Getting zip disk path for {}", artifactEntity.getPath());
|
||||||
|
|
||||||
|
Path zipDiskPath = zipDisk.getPath(artifactEntity.getPath().toString());
|
||||||
|
if (zipDiskPath.getParent() != null && !Files.exists(zipDiskPath.getParent())) {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(zipDiskPath.getParent());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Got zip disk path {}", zipDiskPath);
|
||||||
|
return zipDiskPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileSystem getZipDisk(UUID artifactId) throws IOException {
|
||||||
|
log.debug("Retrieving zip disk for artifact {}", artifactId);
|
||||||
|
URI uri = resolveArtifactUri(artifactId);
|
||||||
|
log.debug("Looking for zip disk at uri {}", uri);
|
||||||
|
|
||||||
|
// make sure nobody closes, deletes or interleavingly opens the filesystem while it is
|
||||||
|
// being opened
|
||||||
|
synchronized (filesystems) {
|
||||||
|
if (!filesystems.containsKey(uri)) {
|
||||||
|
log.debug("Zip disk does not exist. Creating zip disk for {}", uri);
|
||||||
|
FileSystem zipDisk = FileSystems.newFileSystem(uri, Map.of("create", "true"));
|
||||||
|
filesystems.put(uri, zipDisk);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filesystems.get(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeZipDisk(UUID artifactId) throws IOException {
|
||||||
|
log.debug("Closing zip disk for artifact {}", artifactId);
|
||||||
|
URI uri = resolveArtifactUri(artifactId);
|
||||||
|
log.debug("Closing zip disk at uri {}", uri);
|
||||||
|
|
||||||
|
// make sure nobody opens the filesystem while it is being closed
|
||||||
|
synchronized (filesystems) {
|
||||||
|
FileSystem zipDisk = filesystems.remove(uri);
|
||||||
|
if (zipDisk == null) {
|
||||||
|
log.warn("Could not close zip disk at {}. Filesystem not found.", uri);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
zipDisk.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private URI resolveArtifactUri(UUID artifactId) throws IOException {
|
||||||
|
ArtifactEntity artifactEntity = artifactRepository.getOne(artifactId);
|
||||||
|
UUID flingId = artifactEntity.getFling().getId();
|
||||||
|
|
||||||
|
return resolveFlingUri(flingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private URI resolveFlingUri(UUID flingId) throws IOException {
|
||||||
|
Path zipDiskPath = archivePath.resolve(flingId.toString() + ".zip");
|
||||||
|
return URI.create("jar:file:" + zipDiskPath.toFile().getCanonicalPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setArchivePath(String archivePath) {
|
||||||
|
this.archivePath = Paths.get(archivePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setArchivePath(Path archivePath) {
|
||||||
|
this.archivePath = archivePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
{
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"name": "fling.archive.filesystem",
|
||||||
|
"type": "net.friedl.fling.service.archive.impl.FileSystemArchive",
|
||||||
|
"sourceType": "net.friedl.fling.service.archive.impl.FileSystemArchive"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fling.security",
|
||||||
|
"type": "net.friedl.fling.security.FlingWebSecurityConfiguration",
|
||||||
|
"sourceType": "net.friedl.fling.security.FlingWebSecurityConfiguration"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fling.api",
|
||||||
|
"type": "net.friedl.fling.controller.OpenApiConfiguration",
|
||||||
|
"sourceType": "net.friedl.fling.controller.OpenApiConfiguration"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "fling.api.version",
|
||||||
|
"type": "java.lang.String",
|
||||||
|
"description": "Fling API version",
|
||||||
|
"sourceType": "net.friedl.fling.controller.OpenApiConfiguration"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fling.api.server-url",
|
||||||
|
"type": "java.lang.String",
|
||||||
|
"description": "Base URL for the fling api",
|
||||||
|
"sourceType": "net.friedl.fling.controller.OpenApiConfiguration"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fling.api.server-description",
|
||||||
|
"type": "java.lang.String",
|
||||||
|
"description": "A description for the server to be shown in OAS",
|
||||||
|
"sourceType": "net.friedl.fling.controller.OpenApiConfiguration"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fling.archive.filesystem.archive-path",
|
||||||
|
"type": "java.lang.String",
|
||||||
|
"description": "Directory where FileSystemArchive stores its data",
|
||||||
|
"sourceType": "net.friedl.fling.service.archive.impl.FileSystemArchive"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fling.security.allowed-origins",
|
||||||
|
"type": "java.util.List",
|
||||||
|
"description": "Allowed origins for CORS",
|
||||||
|
"sourceType": "net.friedl.fling.security.FlingWebSecurityConfiguration"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fling.security.admin-name",
|
||||||
|
"type": "java.lang.String",
|
||||||
|
"description": "Username of the admin user/instance owner",
|
||||||
|
"sourceType": "net.friedl.fling.security.FlingWebSecurityConfiguration"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fling.security.admin-password",
|
||||||
|
"type": "java.util.String",
|
||||||
|
"description": "Password of the admin user/instance owner",
|
||||||
|
"sourceType": "net.friedl.fling.security.FlingWebSecurityConfiguration"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fling.security.signing-key",
|
||||||
|
"type": "java.util.String",
|
||||||
|
"description": "Key for signing JWT tokens. Must be 256 bits (32 bytes)",
|
||||||
|
"sourceType": "net.friedl.fling.security.FlingWebSecurityConfiguration"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fling.security.jwt-expiration",
|
||||||
|
"type": "java.util.Long",
|
||||||
|
"description": "Time until JWT tokens expire",
|
||||||
|
"sourceType": "net.friedl.fling.security.FlingWebSecurityConfiguration"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -21,14 +21,18 @@ logging.level:
|
||||||
# spring.http.log-request-details: true
|
# spring.http.log-request-details: true
|
||||||
|
|
||||||
fling:
|
fling:
|
||||||
archive.fileystem.directory: "/home/armin/Desktop/fling"
|
archive.filesystem.archive-path: /home/armin/Desktop/fling
|
||||||
security:
|
security:
|
||||||
allowed-origins:
|
allowed-origins:
|
||||||
- "https://friedl.net"
|
- "https://friedl.net"
|
||||||
- "http://localhost:3000"
|
- "http://localhost:3000"
|
||||||
- "http://localhost:5000"
|
- "http://localhost:5000"
|
||||||
- "http://10.0.2.2:5000"
|
- "http://10.0.2.2:5000"
|
||||||
admin-user: "${FLING_ADMIN_USER:admin}"
|
admin-name: "admin"
|
||||||
admin-password: "${FLING_ADMIN_PASSWORD:123}"
|
admin-password: "123"
|
||||||
signing-key: "${FLING_SIGNING_KEY:changeitchangeitchangeitchangeit}"
|
signing-key: "changeitchangeitchangeitchangeit"
|
||||||
jwt-expiration: "${FLING_JWT_EXPIRATION:180000}"
|
jwt-expiration: "180000"
|
||||||
|
api:
|
||||||
|
version: "0.1.0-snapshot"
|
||||||
|
server-url: "http://localhost:8080"
|
||||||
|
server-description: "API server for dev"
|
||||||
|
|
|
@ -16,12 +16,18 @@ logging.level:
|
||||||
root: WARN
|
root: WARN
|
||||||
|
|
||||||
fling:
|
fling:
|
||||||
archive.fileystem.directory: "/var/fling/files"
|
archive.filesystem.archive-path: "/var/fling/files"
|
||||||
security:
|
security:
|
||||||
allowed-origins:
|
allowed-origins:
|
||||||
- "https://fling.friedl.net"
|
- "https://friedl.net"
|
||||||
- "http://localhost:3000"
|
- "http://localhost:3000"
|
||||||
admin-user: "${FLING_ADMIN_USER:admin}"
|
- "http://localhost:5000"
|
||||||
admin-password: "${FLING_ADMIN_PASSWORD:123}"
|
- "http://10.0.2.2:5000"
|
||||||
signing-key: "${FLING_SIGNING_KEY:changeitchangeitchangeitchangeit}"
|
admin-name: "adminName"
|
||||||
jwt-expiration: "${FLING_JWT_EXPIRATION:180000}"
|
admin-password: "adminPassword"
|
||||||
|
signing-key: "changeitchangeitchangeitchangeit"
|
||||||
|
jwt-expiration: "180000"
|
||||||
|
api:
|
||||||
|
version: "0.1.0-snapshot"
|
||||||
|
server-url: "http://localhost:8080"
|
||||||
|
server-description: "API server for dev"
|
|
@ -1 +1,3 @@
|
||||||
spring.profiles.active: "@spring.profiles.active@" # To be replaced by maven according to profile settings
|
# To be replaced by maven
|
||||||
|
spring.profiles.active: "@spring.profiles.active@"
|
||||||
|
fling.api.version: "@fling.api.version@"
|
|
@ -1,11 +1,24 @@
|
||||||
package net.friedl.fling.controller;
|
package net.friedl.fling.controller;
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.equalTo;
|
import static org.hamcrest.CoreMatchers.not;
|
||||||
import static org.hamcrest.Matchers.hasSize;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
import java.util.List;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
import javax.persistence.EntityNotFoundException;
|
||||||
|
import org.hamcrest.Matchers;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
|
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
|
||||||
|
@ -13,9 +26,12 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
import org.springframework.context.annotation.ComponentScan.Filter;
|
import org.springframework.context.annotation.ComponentScan.Filter;
|
||||||
import org.springframework.context.annotation.FilterType;
|
import org.springframework.context.annotation.FilterType;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
import net.friedl.fling.model.dto.ArtifactDto;
|
import net.friedl.fling.model.dto.ArtifactDto;
|
||||||
import net.friedl.fling.service.ArtifactService;
|
import net.friedl.fling.service.ArtifactService;
|
||||||
|
import net.friedl.fling.service.archive.ArchiveService;
|
||||||
|
|
||||||
@WebMvcTest(controllers = ArtifactController.class,
|
@WebMvcTest(controllers = ArtifactController.class,
|
||||||
// do auto-configure security
|
// do auto-configure security
|
||||||
|
@ -29,28 +45,98 @@ class ArtifactControllerTest {
|
||||||
@MockBean
|
@MockBean
|
||||||
private ArtifactService artifactService;
|
private ArtifactService artifactService;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private ArchiveService archiveService;
|
||||||
|
|
||||||
|
private static final UUID ARTIFACT_ID = UUID.randomUUID();
|
||||||
|
|
||||||
|
private ArtifactDto artifactDto =
|
||||||
|
new ArtifactDto(ARTIFACT_ID, Path.of("testArtifact"), Instant.EPOCH, false);
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetArtifacts_noArtifacts_empty() throws Exception {
|
public void getArtifact_noArtifactWithId_notFound() throws Exception {
|
||||||
Long flingId = 123L;
|
when(artifactService.getById(ARTIFACT_ID)).thenThrow(EntityNotFoundException.class);
|
||||||
|
|
||||||
when(artifactService.findAllArtifacts(flingId)).thenReturn(List.of());
|
mvc.perform(get("/api/artifacts/{id}", ARTIFACT_ID))
|
||||||
|
.andExpect(status().isNotFound());
|
||||||
mvc.perform(get("/api/artifacts").param("flingId", flingId.toString()))
|
|
||||||
.andExpect(jsonPath("$", hasSize(0)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetArtifacts_hasArtifacts_allArtifacts() throws Exception {
|
public void getArtifacts_ok() throws Exception {
|
||||||
Long flingId = 123L;
|
when(artifactService.getById(ARTIFACT_ID)).thenReturn(artifactDto);
|
||||||
String artifactName = "TEST";
|
|
||||||
|
|
||||||
ArtifactDto artifactDto = new ArtifactDto();
|
mvc.perform(get("/api/artifacts/{id}", ARTIFACT_ID))
|
||||||
artifactDto.setName(artifactName);
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.id", equalTo(ARTIFACT_ID.toString())));
|
||||||
|
}
|
||||||
|
|
||||||
when(artifactService.findAllArtifacts(flingId)).thenReturn(List.of(artifactDto));
|
@Test
|
||||||
|
public void deleteArtifact_noArtifactWithId_notFound() throws Exception {
|
||||||
|
doThrow(EntityNotFoundException.class).when(artifactService).delete(ARTIFACT_ID);
|
||||||
|
|
||||||
mvc.perform(get("/api/artifacts").param("flingId", flingId.toString()))
|
mvc.perform(delete("/api/artifacts/{id}", ARTIFACT_ID))
|
||||||
.andExpect(jsonPath("$", hasSize(1)))
|
.andExpect(status().isNotFound());
|
||||||
.andExpect(jsonPath("$[0].name", equalTo(artifactName)));
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deleteArtifact_ok() throws Exception {
|
||||||
|
mvc.perform(delete("/api/artifacts/{id}", ARTIFACT_ID))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void uploadArtifact_ioError_serverError() throws Exception {
|
||||||
|
doThrow(IOException.class).when(archiveService).storeArtifact(any(), any());
|
||||||
|
|
||||||
|
byte[] payload = "Payload".getBytes();
|
||||||
|
mvc.perform(post("/api/artifacts/{id}/data", ARTIFACT_ID)
|
||||||
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
|
.content(payload))
|
||||||
|
.andExpect(status().isInternalServerError());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void uploadArtifact_ok() throws Exception {
|
||||||
|
byte[] payload = "Payload".getBytes();
|
||||||
|
mvc.perform(post("/api/artifacts/{id}/data", ARTIFACT_ID)
|
||||||
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
|
.content(payload))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void downloadArtifact_noArtifact_notFound() throws Exception {
|
||||||
|
doThrow(EntityNotFoundException.class).when(artifactService).getById(ARTIFACT_ID);
|
||||||
|
|
||||||
|
mvc.perform(get("/api/artifacts/{id}/data", ARTIFACT_ID))
|
||||||
|
.andExpect(header().doesNotExist(HttpHeaders.CONTENT_DISPOSITION))
|
||||||
|
.andExpect(header().string(HttpHeaders.CONTENT_TYPE,
|
||||||
|
not(equalTo(MediaType.APPLICATION_OCTET_STREAM_VALUE))))
|
||||||
|
.andExpect(status().isNotFound());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void downloadArtifact_ioError_serverError() throws Exception {
|
||||||
|
doThrow(IOException.class).when(archiveService).getArtifact(ARTIFACT_ID);
|
||||||
|
|
||||||
|
mvc.perform(get("/api/artifacts/{id}/data", ARTIFACT_ID))
|
||||||
|
.andExpect(header().doesNotExist(HttpHeaders.CONTENT_DISPOSITION))
|
||||||
|
.andExpect(header().string(HttpHeaders.CONTENT_TYPE,
|
||||||
|
not(equalTo(MediaType.APPLICATION_OCTET_STREAM_VALUE))))
|
||||||
|
.andExpect(status().isInternalServerError());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void downloadArtifact_ok() throws Exception {
|
||||||
|
when(artifactService.getById(ARTIFACT_ID)).thenReturn(artifactDto);
|
||||||
|
byte[] testData = "test".getBytes();
|
||||||
|
when(archiveService.getArtifact(any())).thenReturn(new ByteArrayInputStream(testData));
|
||||||
|
|
||||||
|
mvc.perform(get("/api/artifacts/{id}/data", ARTIFACT_ID))
|
||||||
|
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
|
||||||
|
.andExpect(header().exists(HttpHeaders.CONTENT_DISPOSITION))
|
||||||
|
.andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION,
|
||||||
|
Matchers.containsString("attachment;filename")))
|
||||||
|
.andExpect(content().bytes(testData));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
package net.friedl.fling.controller;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.context.annotation.ComponentScan.Filter;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import net.friedl.fling.model.dto.AdminAuthDto;
|
||||||
|
import net.friedl.fling.model.dto.UserAuthDto;
|
||||||
|
import net.friedl.fling.service.AuthenticationService;
|
||||||
|
import net.friedl.fling.service.AuthorizationService;
|
||||||
|
|
||||||
|
@WebMvcTest(controllers = AuthenticationController.class,
|
||||||
|
includeFilters = {@Filter(Configuration.class)})
|
||||||
|
@ActiveProfiles("local")
|
||||||
|
public class AuthenticationControllerTest {
|
||||||
|
@Autowired
|
||||||
|
private MockMvc mvc;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private AuthenticationService authenticationService;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private AuthorizationService authorizationService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticateOwner_noToken_403() throws Exception {
|
||||||
|
AdminAuthDto adminAuthDto = new AdminAuthDto("admin", "123");
|
||||||
|
when(authenticationService.authenticate(any(AdminAuthDto.class))).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
mvc.perform(post("/api/auth/admin")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(adminAuthDto)))
|
||||||
|
.andExpect(status().is(403));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticateOwner_token_ok() throws Exception {
|
||||||
|
AdminAuthDto adminAuthDto = new AdminAuthDto("admin", "123");
|
||||||
|
when(authenticationService.authenticate(any(AdminAuthDto.class)))
|
||||||
|
.thenReturn(Optional.of("token"));
|
||||||
|
|
||||||
|
mvc.perform(post("/api/auth/admin")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(adminAuthDto)))
|
||||||
|
.andExpect(status().is(200))
|
||||||
|
.andExpect(content().string("token"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticateUser_noToken_403() throws Exception {
|
||||||
|
UserAuthDto userAuthDto = new UserAuthDto("shareId", "authCode");
|
||||||
|
when(authenticationService.authenticate(any(UserAuthDto.class))).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
mvc.perform(post("/api/auth/user")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(userAuthDto)))
|
||||||
|
.andExpect(status().is(403));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticateUser_token_ok() throws Exception {
|
||||||
|
UserAuthDto userAuthDto = new UserAuthDto("shareId", "authCode");
|
||||||
|
when(authenticationService.authenticate(any(UserAuthDto.class)))
|
||||||
|
.thenReturn(Optional.of("token"));
|
||||||
|
|
||||||
|
mvc.perform(post("/api/auth/user")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(userAuthDto)))
|
||||||
|
.andExpect(status().is(200))
|
||||||
|
.andExpect(content().string("token"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,291 @@
|
||||||
|
package net.friedl.fling.controller;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.equalTo;
|
||||||
|
import static org.hamcrest.Matchers.anyOf;
|
||||||
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
|
import static org.hamcrest.Matchers.not;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.doNothing;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import javax.persistence.EntityNotFoundException;
|
||||||
|
import org.hamcrest.Matchers;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.context.annotation.ComponentScan.Filter;
|
||||||
|
import org.springframework.context.annotation.FilterType;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import net.friedl.fling.model.dto.ArtifactDto;
|
||||||
|
import net.friedl.fling.model.dto.FlingDto;
|
||||||
|
import net.friedl.fling.service.ArtifactService;
|
||||||
|
import net.friedl.fling.service.FlingService;
|
||||||
|
import net.friedl.fling.service.archive.ArchiveService;
|
||||||
|
|
||||||
|
@WebMvcTest(controllers = FlingController.class,
|
||||||
|
// do auto-configure security
|
||||||
|
excludeAutoConfiguration = SecurityAutoConfiguration.class,
|
||||||
|
// do not try to create beans in security
|
||||||
|
excludeFilters = @Filter(type = FilterType.REGEX, pattern = "net.friedl.fling.security.*"))
|
||||||
|
public class FlingControllerTest {
|
||||||
|
@Autowired
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ObjectMapper mapper;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private FlingService flingService;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private ArtifactService artifactService;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private ArchiveService archiveService;
|
||||||
|
|
||||||
|
private static final UUID flingId = UUID.randomUUID();
|
||||||
|
|
||||||
|
private FlingDto flingDto = new FlingDto(flingId, "name", Instant.EPOCH, "shareId", "authCode",
|
||||||
|
false, true, true, 1, null);
|
||||||
|
|
||||||
|
private ArtifactDto artifactDto =
|
||||||
|
new ArtifactDto(UUID.randomUUID(), Path.of("testArtifact"), Instant.EPOCH, false);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getFlings_noFlings_empty() throws Exception {
|
||||||
|
when(flingService.findAll()).thenReturn(List.of());
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/fling"))
|
||||||
|
.andExpect(jsonPath("$", hasSize(0)))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getFlings_allFlings() throws Exception {
|
||||||
|
when(flingService.findAll()).thenReturn(List.of(flingDto, flingDto));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/fling"))
|
||||||
|
.andExpect(jsonPath("$", hasSize(2)))
|
||||||
|
.andExpect(jsonPath("$[0].id", equalTo(flingId.toString())))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void postFling_ok() throws Exception {
|
||||||
|
mockMvc.perform(post("/api/fling")
|
||||||
|
.content(mapper.writeValueAsString(flingDto))
|
||||||
|
.contentType(MediaType.APPLICATION_JSON))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void postFling_validatesBody_notOk() throws Exception {
|
||||||
|
FlingDto invalidFlingDto = new FlingDto();
|
||||||
|
|
||||||
|
mockMvc.perform(post("/api/fling")
|
||||||
|
.content(mapper.writeValueAsString(invalidFlingDto))
|
||||||
|
.contentType(MediaType.APPLICATION_JSON))
|
||||||
|
.andExpect(status().isBadRequest());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void replaceFling_validatesBody_notOk() throws Exception {
|
||||||
|
FlingDto invalidFlingDto = new FlingDto();
|
||||||
|
|
||||||
|
mockMvc.perform(put("/api/fling/{id}", flingId)
|
||||||
|
.content(mapper.writeValueAsString(invalidFlingDto))
|
||||||
|
.contentType(MediaType.APPLICATION_JSON))
|
||||||
|
.andExpect(status().isBadRequest());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void replaceFling_ok() throws Exception {
|
||||||
|
FlingDto flingDto = new FlingDto(flingId, "new-name", Instant.EPOCH, "shareId", "new-authCode",
|
||||||
|
false, true, true, 1, null);
|
||||||
|
|
||||||
|
mockMvc.perform(put("/api/fling/{id}", flingId)
|
||||||
|
.content(mapper.writeValueAsString(flingDto))
|
||||||
|
.contentType(MediaType.APPLICATION_JSON))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void postArtifact_ok() throws Exception {
|
||||||
|
mockMvc.perform(post("/api/fling/{id}/artifacts", flingId)
|
||||||
|
.content(mapper.writeValueAsString(artifactDto))
|
||||||
|
.contentType(MediaType.APPLICATION_JSON))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void postArtifact_validatesBody_notOk() throws Exception {
|
||||||
|
ArtifactDto invalidArtifactDto = new ArtifactDto();
|
||||||
|
|
||||||
|
mockMvc.perform(post("/api/fling/{id}/artifacts", flingId)
|
||||||
|
.content(mapper.writeValueAsString(invalidArtifactDto))
|
||||||
|
.contentType(MediaType.APPLICATION_JSON))
|
||||||
|
.andExpect(status().isBadRequest());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getArtifact_noFlingWithId_notFound() throws Exception {
|
||||||
|
doThrow(EntityNotFoundException.class).when(flingService).getArtifacts(flingId);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/fling/{id}/artifacts", flingId))
|
||||||
|
.andExpect(status().isNotFound());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getArtifact_flingFound_noArtifacts_emptySet() throws Exception {
|
||||||
|
when(flingService.getArtifacts(flingId)).thenReturn(Set.of());
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/fling/{id}/artifacts", flingId))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().string(equalTo("[]")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getArtifact_flingFound_hasArtifacts_returnArtifacts() throws Exception {
|
||||||
|
ArtifactDto artifactDto1 = ArtifactDto.builder()
|
||||||
|
.id(new UUID(0, 0))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ArtifactDto artifactDto2 = ArtifactDto.builder()
|
||||||
|
.id(new UUID(0, 1))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
when(flingService.getArtifacts(flingId)).thenReturn(Set.of(artifactDto1, artifactDto2));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/fling/{id}/artifacts", flingId))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$[0].id",
|
||||||
|
anyOf(equalTo(new UUID(0, 0).toString()), equalTo(new UUID(0, 1).toString()))))
|
||||||
|
.andExpect(jsonPath("$[1].id",
|
||||||
|
anyOf(equalTo(new UUID(0, 0).toString()), equalTo(new UUID(0, 1).toString()))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getFling_noFlingWithId_notFound() throws Exception {
|
||||||
|
doThrow(EntityNotFoundException.class).when(flingService).getById(flingId);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/fling/{id}", flingId))
|
||||||
|
.andExpect(status().isNotFound());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getFling_flingFound_returnsFling() throws Exception {
|
||||||
|
when(flingService.getById(flingId)).thenReturn(flingDto);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/fling/{id}", flingId))
|
||||||
|
.andExpect(jsonPath("$.id", equalTo(flingId.toString())))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getFlingByShareId_noFlingWithShareId_notFound() throws Exception {
|
||||||
|
doThrow(EntityNotFoundException.class).when(flingService).getByShareId("doesNotExist");
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/fling/share/{shareId}", "doesNotExist"))
|
||||||
|
.andExpect(status().isNotFound());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getFlingByShareId_flingFind_returnsFling() throws Exception {
|
||||||
|
doReturn(flingDto).when(flingService).getByShareId("shareId");
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/fling/share/{shareId}", "shareId"))
|
||||||
|
.andExpect(jsonPath("$.id", equalTo(flingId.toString())))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deleteFling_noFlingWithId_notFound() throws Exception {
|
||||||
|
doThrow(EntityNotFoundException.class).when(flingService).delete(flingId);
|
||||||
|
|
||||||
|
mockMvc.perform(delete("/api/fling/{id}", flingId))
|
||||||
|
.andExpect(status().isNotFound());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deleteFling_ok() throws Exception {
|
||||||
|
doNothing().when(flingService).delete(flingId);
|
||||||
|
|
||||||
|
mockMvc.perform(delete("/api/fling/{id}", flingId))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getFlingData_ioError_serverError() throws Exception {
|
||||||
|
doThrow(IOException.class).when(archiveService).getFling(flingId);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/fling/{id}/data", flingId))
|
||||||
|
.andExpect(header().doesNotExist(HttpHeaders.CONTENT_DISPOSITION))
|
||||||
|
.andExpect(header().string(HttpHeaders.CONTENT_TYPE,
|
||||||
|
not(equalTo(MediaType.APPLICATION_OCTET_STREAM_VALUE))))
|
||||||
|
.andExpect(status().isInternalServerError());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getFlingData_ok() throws Exception {
|
||||||
|
when(flingService.getById(flingId)).thenReturn(flingDto);
|
||||||
|
int[] testZipInt = new int[] {
|
||||||
|
0x50, 0x4b, 0x03, 0x04, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0x77, 0xe4, 0x50, 0xc6,
|
||||||
|
0x35,
|
||||||
|
0xb9, 0x3b, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x74,
|
||||||
|
0x65,
|
||||||
|
0x73, 0x74, 0x55, 0x54, 0x09, 0x00, 0x03, 0x40, 0x7d, 0x00, 0x5f, 0x37, 0x7d, 0x00, 0x5f,
|
||||||
|
0x75,
|
||||||
|
0x78, 0x0b, 0x00, 0x01, 0x04, 0xe8, 0x03, 0x00, 0x00, 0x04, 0xe8, 0x03, 0x00, 0x00, 0x74,
|
||||||
|
0x65,
|
||||||
|
0x73, 0x74, 0x0a, 0x50, 0x4b, 0x01, 0x02, 0x1e, 0x03, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x76,
|
||||||
|
0x77, 0xe4, 0x50, 0xc6, 0x35, 0xb9, 0x3b, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00,
|
||||||
|
0x04,
|
||||||
|
0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xb4, 0x81, 0x00, 0x00,
|
||||||
|
0x00,
|
||||||
|
0x00, 0x74, 0x65, 0x73, 0x74, 0x55, 0x54, 0x05, 0x00, 0x03, 0x40, 0x7d, 0x00, 0x5f, 0x75,
|
||||||
|
0x78,
|
||||||
|
0x0b, 0x00, 0x01, 0x04, 0xe8, 0x03, 0x00, 0x00, 0x04, 0xe8, 0x03, 0x00, 0x00, 0x50, 0x4b,
|
||||||
|
0x05,
|
||||||
|
0x06, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x43, 0x00,
|
||||||
|
0x00,
|
||||||
|
0x00, 0x00, 0x00
|
||||||
|
};
|
||||||
|
byte[] testZip = new byte[testZipInt.length];
|
||||||
|
for (int idx = 0; idx < testZip.length; idx++) testZip[idx] = (byte) testZipInt[idx];
|
||||||
|
|
||||||
|
when(archiveService.getFling(any()))
|
||||||
|
.thenAnswer((invocation) -> {
|
||||||
|
// need to use thenAnswer here to always return a fresh new (unclosed) input stream
|
||||||
|
return new ByteArrayInputStream(testZip);
|
||||||
|
});
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/fling/{id}/data", flingId))
|
||||||
|
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
|
||||||
|
.andExpect(header().exists(HttpHeaders.CONTENT_DISPOSITION))
|
||||||
|
.andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION,
|
||||||
|
Matchers.containsString("attachment;filename")))
|
||||||
|
.andExpect(content().bytes(testZip));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package net.friedl.fling.model;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import javax.validation.ConstraintViolation;
|
||||||
|
import javax.validation.Validator;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
|
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||||
|
import net.friedl.fling.model.dto.ArtifactDto;
|
||||||
|
|
||||||
|
@ExtendWith(SpringExtension.class)
|
||||||
|
@ContextConfiguration(classes = ModelTestConfiguration.class)
|
||||||
|
public class ArtifactDtoTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Validator validator;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetId_null_validationOk() {
|
||||||
|
ArtifactDto artifactDto = ArtifactDto.builder()
|
||||||
|
.id(null)
|
||||||
|
.path(Paths.get("test"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
|
||||||
|
Set<ConstraintViolation<ArtifactDto>> constraintViolations = validator.validate(artifactDto);
|
||||||
|
|
||||||
|
assertThat(constraintViolations).hasSize(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetPath_null_validationFails() {
|
||||||
|
ArtifactDto artifactDto = ArtifactDto.builder()
|
||||||
|
.id(new UUID(0L, 0L))
|
||||||
|
.path(null)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
|
||||||
|
Set<ConstraintViolation<ArtifactDto>> constraintViolations = validator.validate(artifactDto);
|
||||||
|
|
||||||
|
assertThat(constraintViolations).hasSize(1);
|
||||||
|
ConstraintViolation<ArtifactDto> violation = constraintViolations.iterator().next();
|
||||||
|
assertThat(violation.getPropertyPath().toString()).isEqualTo("path");
|
||||||
|
assertThat(violation.getMessage()).isEqualTo("must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMandatoryFieldsSet_validationOk() {
|
||||||
|
ArtifactDto artifactDto = ArtifactDto.builder()
|
||||||
|
.id(new UUID(0L, 0L))
|
||||||
|
.path(Paths.get("test"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Set<ConstraintViolation<ArtifactDto>> constraintViolations = validator.validate(artifactDto);
|
||||||
|
assertTrue(constraintViolations.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
package net.friedl.fling.model;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import javax.validation.ConstraintViolation;
|
||||||
|
import javax.validation.Validator;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
|
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||||
|
import net.friedl.fling.model.dto.FlingDto;
|
||||||
|
|
||||||
|
@ExtendWith(SpringExtension.class)
|
||||||
|
@ContextConfiguration(classes = ModelTestConfiguration.class)
|
||||||
|
public class FlingDtoTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Validator validator;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetId_null_validationOk() {
|
||||||
|
FlingDto flingDto = FlingDto.builder()
|
||||||
|
.id(null)
|
||||||
|
.name("test")
|
||||||
|
.creationTime(Instant.EPOCH)
|
||||||
|
.shareId("test")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
|
||||||
|
Set<ConstraintViolation<FlingDto>> constraintViolations = validator.validate(flingDto);
|
||||||
|
|
||||||
|
assertThat(constraintViolations).hasSize(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetName_null_validationFails() {
|
||||||
|
FlingDto flingDto = FlingDto.builder()
|
||||||
|
.id(new UUID(0L, 0L))
|
||||||
|
.name(null)
|
||||||
|
.creationTime(Instant.EPOCH)
|
||||||
|
.shareId("test")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
|
||||||
|
Set<ConstraintViolation<FlingDto>> constraintViolations = validator.validate(flingDto);
|
||||||
|
|
||||||
|
assertThat(constraintViolations).hasSize(1);
|
||||||
|
ConstraintViolation<FlingDto> violation = constraintViolations.iterator().next();
|
||||||
|
assertThat(violation.getPropertyPath().toString()).isEqualTo("name");
|
||||||
|
assertThat(violation.getMessage()).isEqualTo("must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetCreationTime_null_validationOk() {
|
||||||
|
FlingDto flingDto = FlingDto.builder()
|
||||||
|
.id(new UUID(0L, 0L))
|
||||||
|
.name("test")
|
||||||
|
.creationTime(null)
|
||||||
|
.shareId("test")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
|
||||||
|
Set<ConstraintViolation<FlingDto>> constraintViolations = validator.validate(flingDto);
|
||||||
|
|
||||||
|
assertThat(constraintViolations).hasSize(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetShareId_null_validationOk() { // must be nullable to support defaulting in service
|
||||||
|
FlingDto flingDto = FlingDto.builder()
|
||||||
|
.id(new UUID(0L, 0L))
|
||||||
|
.name("test")
|
||||||
|
.creationTime(Instant.EPOCH)
|
||||||
|
.shareId(null)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
|
||||||
|
Set<ConstraintViolation<FlingDto>> constraintViolations = validator.validate(flingDto);
|
||||||
|
|
||||||
|
assertThat(constraintViolations).hasSize(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetAllManadatory_validationOk() {
|
||||||
|
FlingDto flingDto = FlingDto.builder()
|
||||||
|
.id(new UUID(0L, 0L))
|
||||||
|
.name("test")
|
||||||
|
.creationTime(Instant.EPOCH)
|
||||||
|
.shareId("test")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
|
||||||
|
Set<ConstraintViolation<FlingDto>> constraintViolations = validator.validate(flingDto);
|
||||||
|
assertTrue(constraintViolations.isEmpty());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package net.friedl.fling.model;
|
||||||
|
|
||||||
|
import javax.validation.Validator;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class ModelTestConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Validator validator() {
|
||||||
|
LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();
|
||||||
|
localValidatorFactoryBean.afterPropertiesSet();
|
||||||
|
return localValidatorFactoryBean;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
package net.friedl.fling.service;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.equalTo;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.invocation.InvocationOnMock;
|
||||||
|
import org.mockito.stubbing.Answer;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.TestConfiguration;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||||
|
import net.friedl.fling.model.dto.ArtifactDto;
|
||||||
|
import net.friedl.fling.model.mapper.ArtifactMapper;
|
||||||
|
import net.friedl.fling.model.mapper.ArtifactMapperImpl;
|
||||||
|
import net.friedl.fling.persistence.entities.ArtifactEntity;
|
||||||
|
import net.friedl.fling.persistence.entities.FlingEntity;
|
||||||
|
import net.friedl.fling.persistence.repositories.ArtifactRepository;
|
||||||
|
import net.friedl.fling.persistence.repositories.FlingRepository;
|
||||||
|
import net.friedl.fling.service.archive.ArchiveService;
|
||||||
|
|
||||||
|
@ExtendWith(SpringExtension.class)
|
||||||
|
public class ArtifactServiceTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ArtifactService artifactService;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private FlingRepository flingRepository;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private ArtifactRepository artifactRepository;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private ArchiveService archiveService;
|
||||||
|
|
||||||
|
private FlingEntity flingEntity;
|
||||||
|
|
||||||
|
private ArtifactEntity artifactEntity1;
|
||||||
|
|
||||||
|
private ArtifactEntity artifactEntity2;
|
||||||
|
|
||||||
|
@TestConfiguration
|
||||||
|
static class FlingServiceTestConfiguration {
|
||||||
|
@Bean
|
||||||
|
public ArtifactMapper artifactMapper() {
|
||||||
|
return new ArtifactMapperImpl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ArtifactService artifactService(ArtifactRepository artifactRepository,
|
||||||
|
FlingRepository flingRepository, ArtifactMapper artifactMapper,
|
||||||
|
ArchiveService archiveService) {
|
||||||
|
|
||||||
|
return new ArtifactService(artifactRepository, flingRepository, artifactMapper,
|
||||||
|
archiveService);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void beforeEach() {
|
||||||
|
this.artifactEntity1 = new ArtifactEntity();
|
||||||
|
artifactEntity1.setId(UUID.randomUUID());
|
||||||
|
artifactEntity1.setPath(Path.of("artifact1"));
|
||||||
|
|
||||||
|
this.artifactEntity2 = new ArtifactEntity();
|
||||||
|
artifactEntity2.setId(UUID.randomUUID());
|
||||||
|
artifactEntity2.setPath(Path.of("/", "/sub", "artifact2"));
|
||||||
|
|
||||||
|
this.flingEntity = new FlingEntity();
|
||||||
|
flingEntity.setId(UUID.randomUUID());
|
||||||
|
flingEntity.setName("fling");
|
||||||
|
|
||||||
|
when(flingRepository.save(any())).then(new Answer<FlingEntity>() {
|
||||||
|
@Override
|
||||||
|
public FlingEntity answer(InvocationOnMock invocation) throws Throwable {
|
||||||
|
FlingEntity flingEntity = invocation.getArgument(0);
|
||||||
|
if (flingEntity.getId() == null) flingEntity.setId(UUID.randomUUID());
|
||||||
|
return flingEntity;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
when(artifactRepository.save(any())).then(new Answer<ArtifactEntity>() {
|
||||||
|
@Override
|
||||||
|
public ArtifactEntity answer(InvocationOnMock invocation) throws Throwable {
|
||||||
|
ArtifactEntity artifactEntity = invocation.getArgument(0);
|
||||||
|
artifactEntity.setId(UUID.randomUUID());
|
||||||
|
return artifactEntity;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getById_artifactExists_ok() {
|
||||||
|
when(artifactRepository.getOne(artifactEntity1.getId())).thenReturn(artifactEntity1);
|
||||||
|
|
||||||
|
ArtifactDto artifactDto = artifactService.getById(artifactEntity1.getId());
|
||||||
|
assertThat(artifactDto.getId(), equalTo(artifactEntity1.getId()));
|
||||||
|
assertThat(artifactDto.getPath(), equalTo(artifactEntity1.getPath()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void create_createsArtifact_ok() {
|
||||||
|
ArtifactDto artifactToCreate = ArtifactDto.builder()
|
||||||
|
.path(Path.of("new", "artifacts"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ArtifactDto createdArtifact = artifactService.create(flingEntity.getId(), artifactToCreate);
|
||||||
|
|
||||||
|
assertThat(createdArtifact.getPath(), equalTo(artifactToCreate.getPath()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void delete_deletesArchiveAndArtifactEntry() throws IOException {
|
||||||
|
artifactService.delete(artifactEntity1.getId());
|
||||||
|
|
||||||
|
verify(archiveService).deleteArtifact(artifactEntity1.getId());
|
||||||
|
verify(artifactRepository).deleteById(artifactEntity1.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,300 @@
|
||||||
|
package net.friedl.fling.service;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.emptyOrNullString;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.hamcrest.Matchers.hasItem;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.hamcrest.Matchers.not;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import java.security.Key;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import javax.persistence.EntityNotFoundException;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.TestConfiguration;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.core.context.SecurityContextImpl;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
import org.springframework.test.context.TestPropertySource;
|
||||||
|
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import io.jsonwebtoken.Jws;
|
||||||
|
import io.jsonwebtoken.JwtParser;
|
||||||
|
import io.jsonwebtoken.impl.DefaultClaims;
|
||||||
|
import io.jsonwebtoken.impl.DefaultJws;
|
||||||
|
import io.jsonwebtoken.impl.DefaultJwsHeader;
|
||||||
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
import net.friedl.fling.model.dto.AdminAuthDto;
|
||||||
|
import net.friedl.fling.model.dto.UserAuthDto;
|
||||||
|
import net.friedl.fling.persistence.entities.FlingEntity;
|
||||||
|
import net.friedl.fling.persistence.entities.TokenEntity;
|
||||||
|
import net.friedl.fling.persistence.repositories.FlingRepository;
|
||||||
|
import net.friedl.fling.persistence.repositories.TokenRepository;
|
||||||
|
import net.friedl.fling.security.FlingAuthorities;
|
||||||
|
import net.friedl.fling.security.authentication.FlingToken;
|
||||||
|
import net.friedl.fling.security.authentication.authorities.FlingAdminAuthority;
|
||||||
|
import net.friedl.fling.security.authentication.authorities.FlingUserAuthority;
|
||||||
|
|
||||||
|
@ExtendWith(SpringExtension.class)
|
||||||
|
@TestPropertySource("classpath:/application-test.properties")
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
public class AuthenticationServiceTest {
|
||||||
|
@Autowired
|
||||||
|
public AuthenticationService authenticationService;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private FlingRepository flingRepository;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private TokenRepository tokenRepository;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private JwtParser jwtParser;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
@TestConfiguration
|
||||||
|
static class FlingServiceTestConfiguration {
|
||||||
|
private Key jwtSigningKey = Keys.hmacShaKeyFor(new byte[32]);
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public AuthenticationService authenticationService(JwtParser jwtParser,
|
||||||
|
PasswordEncoder passwordEncoder, FlingRepository flingRepository,
|
||||||
|
TokenRepository tokenRepository) {
|
||||||
|
|
||||||
|
return new AuthenticationService(jwtParser, jwtSigningKey, passwordEncoder, flingRepository,
|
||||||
|
tokenRepository);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticate_adminNameDiffers_empty() {
|
||||||
|
AdminAuthDto adminAuthDto = new AdminAuthDto("wrongadmin", "123");
|
||||||
|
|
||||||
|
assertThat(authenticationService.authenticate(adminAuthDto), equalTo(Optional.empty()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticate_passwordDiffers_empty() {
|
||||||
|
AdminAuthDto adminAuthDto = new AdminAuthDto("admin", "wrongpassword");
|
||||||
|
|
||||||
|
assertThat(authenticationService.authenticate(adminAuthDto), equalTo(Optional.empty()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticate_ok() {
|
||||||
|
AdminAuthDto adminAuthDto = new AdminAuthDto("admin", "123");
|
||||||
|
|
||||||
|
assertThat(authenticationService.authenticate(adminAuthDto), not(equalTo(Optional.empty())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticate_authCodeDiffers_empty() {
|
||||||
|
FlingEntity flingEntity = new FlingEntity();
|
||||||
|
flingEntity.setAuthCode("test");
|
||||||
|
flingEntity.setId(UUID.randomUUID());
|
||||||
|
|
||||||
|
UserAuthDto userAuthDto = new UserAuthDto("shareId", "wrongCode");
|
||||||
|
|
||||||
|
when(flingRepository.findByShareId(any(String.class))).thenReturn(flingEntity);
|
||||||
|
when(passwordEncoder.encode(any(String.class))).thenReturn("wrongCode");
|
||||||
|
|
||||||
|
assertThat(authenticationService.authenticate(userAuthDto), equalTo(Optional.empty()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticate_authCodeEmpty_ok() {
|
||||||
|
FlingEntity flingEntity = new FlingEntity();
|
||||||
|
UserAuthDto userAuthDto = new UserAuthDto("shareId", "");
|
||||||
|
|
||||||
|
when(flingRepository.findByShareId(any(String.class))).thenReturn(flingEntity);
|
||||||
|
|
||||||
|
assertThat(authenticationService.authenticate(userAuthDto), not(equalTo(Optional.empty())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticate_authCodeEquals_ok() {
|
||||||
|
FlingEntity flingEntity = new FlingEntity();
|
||||||
|
flingEntity.setAuthCode("authCodeHash");
|
||||||
|
flingEntity.setId(UUID.randomUUID());
|
||||||
|
|
||||||
|
UserAuthDto userAuthDto = UserAuthDto.builder()
|
||||||
|
.authCode("authCode")
|
||||||
|
.shareId("shareId").build();
|
||||||
|
|
||||||
|
when(flingRepository.findByShareId(any(String.class))).thenReturn(flingEntity);
|
||||||
|
when(passwordEncoder.encode(any(String.class))).thenReturn("authCodeHash");
|
||||||
|
when(passwordEncoder.matches("authCode", "authCodeHash")).thenReturn(true);
|
||||||
|
|
||||||
|
assertThat(authenticationService.authenticate(userAuthDto), not(equalTo(Optional.empty())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticate_noFlingForShareId_throws() {
|
||||||
|
UserAuthDto userAuthDto = UserAuthDto.builder()
|
||||||
|
.authCode("authCode")
|
||||||
|
.shareId("doesNotExist").build();
|
||||||
|
|
||||||
|
when(flingRepository.findByShareId(any(String.class))).thenReturn(null);
|
||||||
|
when(passwordEncoder.encode(any(String.class))).thenReturn("authCodeHash");
|
||||||
|
|
||||||
|
assertThrows(EntityNotFoundException.class,
|
||||||
|
() -> authenticationService.authenticate(userAuthDto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseJwtAuthentication_owner_AdminAuthority() {
|
||||||
|
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
|
||||||
|
new DefaultClaims(Map.of("sub", "admin")), "signature");
|
||||||
|
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
|
||||||
|
|
||||||
|
FlingToken flingToken = authenticationService.parseJwtAuthentication("any");
|
||||||
|
assertThat(flingToken.isAuthenticated(), equalTo(true));
|
||||||
|
// authorized for any fling
|
||||||
|
assertThat(flingToken.authorizedForFling(UUID.randomUUID()), equalTo(true));
|
||||||
|
assertThat(flingToken.getCredentials(), equalTo("any"));
|
||||||
|
assertThat(flingToken.getAuthorities(),
|
||||||
|
hasItem(org.hamcrest.Matchers.any(FlingAdminAuthority.class)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseJwtAuthentication_user_UserAuthorityForId() {
|
||||||
|
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
|
||||||
|
new DefaultClaims(Map.of("sub", "user", "id", new UUID(0, 0).toString())), "signature");
|
||||||
|
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
|
||||||
|
|
||||||
|
FlingToken flingToken = authenticationService.parseJwtAuthentication("any");
|
||||||
|
assertThat(flingToken.isAuthenticated(), equalTo(true));
|
||||||
|
// authorized for fling in token
|
||||||
|
assertThat(flingToken.authorizedForFling(new UUID(0, 0)), equalTo(true));
|
||||||
|
// not authorized for fling other flings
|
||||||
|
assertThat(flingToken.authorizedForFling(new UUID(0, 1)), equalTo(false));
|
||||||
|
assertThat(flingToken.getCredentials(), equalTo("any"));
|
||||||
|
assertThat(flingToken.getAuthorities(),
|
||||||
|
hasItem(org.hamcrest.Matchers.any(FlingUserAuthority.class)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseJwtAuthentication_unknownSubject_throws() {
|
||||||
|
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
|
||||||
|
new DefaultClaims(Map.of("sub", "unknownSubject")), "signature");
|
||||||
|
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
|
||||||
|
|
||||||
|
assertThrows(BadCredentialsException.class,
|
||||||
|
() -> authenticationService.parseJwtAuthentication("any"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deriveToken_noAuthenticationInSecurityContext_throws() {
|
||||||
|
assertThrows(IllegalStateException.class,
|
||||||
|
() -> authenticationService.deriveToken(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deriveToken_authenticationInSecurityContext_ok() {
|
||||||
|
FlingToken flingToken = new FlingToken(List.of(new FlingAdminAuthority()), "token");
|
||||||
|
SecurityContextHolder.setContext(new SecurityContextImpl(flingToken));
|
||||||
|
|
||||||
|
String derivedToken = authenticationService.deriveToken(null);
|
||||||
|
|
||||||
|
assertThat(derivedToken, is(not(emptyOrNullString())));
|
||||||
|
SecurityContextHolder.clearContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deriveToken_singleUseNotSet_singleUseIsTrue() {
|
||||||
|
FlingToken flingToken = new FlingToken(List.of(new FlingAdminAuthority()), "token");
|
||||||
|
SecurityContextHolder.setContext(new SecurityContextImpl(flingToken));
|
||||||
|
|
||||||
|
ArgumentCaptor<TokenEntity> tokenEntityCaptor = ArgumentCaptor.forClass(TokenEntity.class);
|
||||||
|
|
||||||
|
authenticationService.deriveToken(null);
|
||||||
|
|
||||||
|
verify(tokenRepository).save(tokenEntityCaptor.capture());
|
||||||
|
assertThat(tokenEntityCaptor.getValue().getSingleUse(), is(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deriveToken_singleUseFalse_singleUseIsFalse() {
|
||||||
|
FlingToken flingToken = new FlingToken(List.of(new FlingAdminAuthority()), "token");
|
||||||
|
SecurityContextHolder.setContext(new SecurityContextImpl(flingToken));
|
||||||
|
|
||||||
|
ArgumentCaptor<TokenEntity> tokenEntityCaptor = ArgumentCaptor.forClass(TokenEntity.class);
|
||||||
|
|
||||||
|
authenticationService.deriveToken(false);
|
||||||
|
|
||||||
|
verify(tokenRepository).save(tokenEntityCaptor.capture());
|
||||||
|
assertThat(tokenEntityCaptor.getValue().getSingleUse(), is(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseDerivedToken_singleUse_deletesToken() {
|
||||||
|
String token = UUID.randomUUID().toString();
|
||||||
|
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
|
||||||
|
new DefaultClaims(Map.of("sub", "admin")), "signature");
|
||||||
|
TokenEntity tokenEntity = new TokenEntity();
|
||||||
|
tokenEntity.setId(UUID.fromString(token));
|
||||||
|
tokenEntity.setSingleUse(true);
|
||||||
|
tokenEntity.setToken("jwtToken");
|
||||||
|
|
||||||
|
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
|
||||||
|
when(tokenRepository.getOne(any(UUID.class))).thenReturn(tokenEntity);
|
||||||
|
|
||||||
|
authenticationService.parseDerivedToken(token);
|
||||||
|
|
||||||
|
verify(tokenRepository).delete(tokenEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseDerivedToken_singleUseFalse_doesNotDeleteToken() {
|
||||||
|
String token = UUID.randomUUID().toString();
|
||||||
|
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
|
||||||
|
new DefaultClaims(Map.of("sub", "admin")), "signature");
|
||||||
|
TokenEntity tokenEntity = new TokenEntity();
|
||||||
|
tokenEntity.setId(UUID.fromString(token));
|
||||||
|
tokenEntity.setSingleUse(false);
|
||||||
|
tokenEntity.setToken("jwtToken");
|
||||||
|
|
||||||
|
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
|
||||||
|
when(tokenRepository.getOne(any(UUID.class))).thenReturn(tokenEntity);
|
||||||
|
|
||||||
|
authenticationService.parseDerivedToken(token);
|
||||||
|
|
||||||
|
verify(tokenRepository, never()).delete(tokenEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseDerivedToken_returnsParentAuthentication() {
|
||||||
|
String token = UUID.randomUUID().toString();
|
||||||
|
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
|
||||||
|
new DefaultClaims(Map.of("sub", "admin")), "signature");
|
||||||
|
TokenEntity tokenEntity = new TokenEntity();
|
||||||
|
tokenEntity.setId(UUID.fromString(token));
|
||||||
|
tokenEntity.setSingleUse(true);
|
||||||
|
tokenEntity.setToken("jwtToken");
|
||||||
|
|
||||||
|
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
|
||||||
|
when(tokenRepository.getOne(any(UUID.class))).thenReturn(tokenEntity);
|
||||||
|
|
||||||
|
FlingToken flingToken = authenticationService.parseDerivedToken(token);
|
||||||
|
|
||||||
|
assertEquals(flingToken.getAuthorities().stream().findFirst().get().getAuthority(),
|
||||||
|
FlingAuthorities.FLING_ADMIN.getAuthority());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
package net.friedl.fling.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import javax.persistence.EntityNotFoundException;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.TestConfiguration;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||||
|
import net.friedl.fling.persistence.entities.FlingEntity;
|
||||||
|
import net.friedl.fling.persistence.repositories.FlingRepository;
|
||||||
|
import net.friedl.fling.security.authentication.FlingToken;
|
||||||
|
import net.friedl.fling.security.authentication.authorities.FlingAdminAuthority;
|
||||||
|
import net.friedl.fling.security.authentication.authorities.FlingUserAuthority;
|
||||||
|
|
||||||
|
@ExtendWith(SpringExtension.class)
|
||||||
|
public class AuthorizationServiceTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AuthorizationService authorizationService;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private FlingRepository flingRepository;
|
||||||
|
|
||||||
|
@TestConfiguration
|
||||||
|
static class FlingServiceTestConfiguration {
|
||||||
|
@Bean
|
||||||
|
public AuthorizationService authorizationService(FlingRepository flingRepository) {
|
||||||
|
return new AuthorizationService(flingRepository);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void allowUpload_unknownToken_false() {
|
||||||
|
var unkownToken = new AnonymousAuthenticationToken("key", "principal",
|
||||||
|
List.of(new SimpleGrantedAuthority("role")));
|
||||||
|
|
||||||
|
assertFalse(authorizationService.allowUpload(UUID.randomUUID(), unkownToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void allowUpload_flingAdmin_true() {
|
||||||
|
FlingToken flingToken = new FlingToken(List.of(new FlingAdminAuthority()), "jwtToken");
|
||||||
|
assertTrue(authorizationService.allowUpload(UUID.randomUUID(), flingToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void allowUpload_noAdmin_uploadDisallowed_false() {
|
||||||
|
FlingEntity flingEntity = new FlingEntity();
|
||||||
|
flingEntity.setAllowUpload(false);
|
||||||
|
|
||||||
|
FlingToken flingToken =
|
||||||
|
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
|
||||||
|
|
||||||
|
when(flingRepository.getOne(new UUID(0, 0))).thenReturn(flingEntity);
|
||||||
|
|
||||||
|
assertFalse(authorizationService.allowUpload(new UUID(0, 0), flingToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void allowUpload_noAdmin_uploadAllowed_notAuthorized_false() {
|
||||||
|
FlingEntity flingEntity = new FlingEntity();
|
||||||
|
flingEntity.setAllowUpload(true);
|
||||||
|
|
||||||
|
FlingToken flingToken =
|
||||||
|
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
|
||||||
|
|
||||||
|
when(flingRepository.getOne(new UUID(1, 1))).thenReturn(flingEntity);
|
||||||
|
|
||||||
|
// Token: UUID(0,0), Request: UUID(1,1)
|
||||||
|
assertFalse(authorizationService.allowUpload(new UUID(1, 1), flingToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void allowUpload_noAdmin_uploadAllowed_authorized_true() {
|
||||||
|
FlingEntity flingEntity = new FlingEntity();
|
||||||
|
flingEntity.setAllowUpload(true);
|
||||||
|
|
||||||
|
FlingToken flingToken =
|
||||||
|
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
|
||||||
|
|
||||||
|
when(flingRepository.getOne(new UUID(0, 0))).thenReturn(flingEntity);
|
||||||
|
|
||||||
|
// Token: UUID(0,0), Request: UUID(0,0)
|
||||||
|
assertTrue(authorizationService.allowUpload(new UUID(0, 0), flingToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void allowFlingAccess_unknownToken_false() {
|
||||||
|
var unkownToken = new AnonymousAuthenticationToken("key", "principal",
|
||||||
|
List.of(new SimpleGrantedAuthority("role")));
|
||||||
|
assertFalse(authorizationService.allowFlingAccess(UUID.randomUUID(), unkownToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void allowFlingAcess_flingAdmin_true() {
|
||||||
|
FlingToken flingToken = new FlingToken(List.of(new FlingAdminAuthority()), "jwtToken");
|
||||||
|
assertTrue(authorizationService.allowFlingAccess(UUID.randomUUID(), flingToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void allowFlingAcess_flingUser_notAuthorizedForId_false() {
|
||||||
|
FlingToken flingToken =
|
||||||
|
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
|
||||||
|
assertFalse(authorizationService.allowFlingAccess(new UUID(1, 1), flingToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void allowFlingAcess_flingUser_authorizedForId_true() {
|
||||||
|
FlingToken flingToken =
|
||||||
|
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
|
||||||
|
assertTrue(authorizationService.allowFlingAccess(new UUID(0, 0), flingToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void allowFlingAccessByShareId_noFlingForShareId_throw() {
|
||||||
|
FlingToken flingToken =
|
||||||
|
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
|
||||||
|
when(flingRepository.findByShareId(any(String.class))).thenReturn(null);
|
||||||
|
|
||||||
|
assertThrows(EntityNotFoundException.class,
|
||||||
|
() -> authorizationService.allowFlingAccessByShareId("doesNotExist", flingToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,263 @@
|
||||||
|
package net.friedl.fling.service;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.equalTo;
|
||||||
|
import static org.hamcrest.CoreMatchers.hasItems;
|
||||||
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.contains;
|
||||||
|
import static org.hamcrest.Matchers.empty;
|
||||||
|
import static org.hamcrest.Matchers.emptyOrNullString;
|
||||||
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
|
import static org.hamcrest.Matchers.not;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.atLeast;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import javax.persistence.EntityNotFoundException;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.invocation.InvocationOnMock;
|
||||||
|
import org.mockito.stubbing.Answer;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.TestConfiguration;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||||
|
import net.friedl.fling.model.dto.ArtifactDto;
|
||||||
|
import net.friedl.fling.model.dto.FlingDto;
|
||||||
|
import net.friedl.fling.model.mapper.ArtifactMapper;
|
||||||
|
import net.friedl.fling.model.mapper.ArtifactMapperImpl;
|
||||||
|
import net.friedl.fling.model.mapper.FlingMapper;
|
||||||
|
import net.friedl.fling.model.mapper.FlingMapperImpl;
|
||||||
|
import net.friedl.fling.persistence.entities.ArtifactEntity;
|
||||||
|
import net.friedl.fling.persistence.entities.FlingEntity;
|
||||||
|
import net.friedl.fling.persistence.repositories.FlingRepository;
|
||||||
|
import net.friedl.fling.service.archive.ArchiveService;
|
||||||
|
|
||||||
|
@ExtendWith(SpringExtension.class)
|
||||||
|
public class FlingServiceTest {
|
||||||
|
@Autowired
|
||||||
|
private FlingService flingService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FlingMapper flingMapper;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private FlingRepository flingRepository;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private ArchiveService archiveService;
|
||||||
|
|
||||||
|
private FlingEntity flingEntity1;
|
||||||
|
|
||||||
|
private FlingEntity flingEntity2;
|
||||||
|
|
||||||
|
@TestConfiguration
|
||||||
|
static class FlingServiceTestConfiguration {
|
||||||
|
@Bean
|
||||||
|
public FlingMapper flingMapper() {
|
||||||
|
return new FlingMapperImpl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ArtifactMapper ArtifactMapper() {
|
||||||
|
return new ArtifactMapperImpl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public FlingService flingService(FlingRepository flingRepository, FlingMapper flingMapper,
|
||||||
|
ArtifactMapper artifactMapper,
|
||||||
|
ArchiveService archiveService, PasswordEncoder passwordEncoder) {
|
||||||
|
return new FlingService(flingRepository, flingMapper, artifactMapper, archiveService,
|
||||||
|
passwordEncoder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void beforeEach() {
|
||||||
|
this.flingEntity1 = new FlingEntity();
|
||||||
|
flingEntity1.setId(UUID.randomUUID());
|
||||||
|
flingEntity1.setName("fling1");
|
||||||
|
flingEntity1.setAuthCode("testhash");
|
||||||
|
|
||||||
|
this.flingEntity2 = new FlingEntity();
|
||||||
|
flingEntity2.setId(UUID.randomUUID());
|
||||||
|
flingEntity2.setName("fling2");
|
||||||
|
flingEntity2.setShareId("shareId2");
|
||||||
|
|
||||||
|
when(flingRepository.save(any())).then(new Answer<FlingEntity>() {
|
||||||
|
@Override
|
||||||
|
public FlingEntity answer(InvocationOnMock invocation) throws Throwable {
|
||||||
|
FlingEntity flingEntity = invocation.getArgument(0);
|
||||||
|
flingEntity.setId(UUID.randomUUID());
|
||||||
|
return flingEntity;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void findAll_noFlings_empty() {
|
||||||
|
when(flingRepository.findAll()).thenReturn(List.of());
|
||||||
|
|
||||||
|
assertThat(flingService.findAll(), is(empty()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void findAll_hasFlings_allFlings() {
|
||||||
|
when(flingRepository.findAll()).thenReturn(List.of(flingEntity1, flingEntity2));
|
||||||
|
|
||||||
|
assertThat(flingService.findAll(), hasItems(
|
||||||
|
flingMapper.map(flingEntity1), flingMapper.map(flingEntity2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getById_flingDto() {
|
||||||
|
when(flingRepository.getOne(flingEntity1.getId())).thenReturn(flingEntity1);
|
||||||
|
assertThat(flingService.getById(flingEntity1.getId()), equalTo(flingMapper.map(flingEntity1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void create_emptyFling_defaultValues() {
|
||||||
|
FlingDto flingDto = new FlingDto();
|
||||||
|
|
||||||
|
FlingDto createdFling = flingService.create(flingDto);
|
||||||
|
assertThat(createdFling.getShareId(), not(emptyOrNullString()));
|
||||||
|
assertThat(createdFling.getAuthCode(), emptyOrNullString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void create_hasAuthCode_setAuthCode() {
|
||||||
|
FlingDto flingDto = new FlingDto();
|
||||||
|
flingDto.setAuthCode("test");
|
||||||
|
|
||||||
|
when(passwordEncoder.encode(any(String.class))).thenReturn("testhash");
|
||||||
|
|
||||||
|
FlingDto createdFling = flingService.create(flingDto);
|
||||||
|
assertThat(createdFling.getAuthCode(), is("testhash"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void create_hasShareId_setShareId() {
|
||||||
|
FlingDto flingDto = new FlingDto();
|
||||||
|
flingDto.setShareId("test");
|
||||||
|
|
||||||
|
FlingDto createdFling = flingService.create(flingDto);
|
||||||
|
assertThat(createdFling.getShareId(), is("test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void replace_newName_expirationClicks_setsNameAndExpirationClicks() {
|
||||||
|
FlingDto flingDto = FlingDto.builder()
|
||||||
|
.name("testName")
|
||||||
|
.authCode(flingEntity1.getAuthCode())
|
||||||
|
.expirationClicks(2)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
when(flingRepository.getOne(flingEntity1.getId())).thenReturn(flingEntity1);
|
||||||
|
FlingDto updatedEntity = flingService.replace(flingEntity1.getId(), flingDto);
|
||||||
|
|
||||||
|
assertThat(updatedEntity.getId(), is(flingEntity1.getId()));
|
||||||
|
assertThat(updatedEntity.getName(), is("testName"));
|
||||||
|
assertThat(updatedEntity.getAuthCode(), is(flingEntity1.getAuthCode()));
|
||||||
|
assertThat(updatedEntity.getExpirationClicks(), is(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void replace_newAuthCode_setsNewHashedAuthCode() {
|
||||||
|
FlingDto flingDto = FlingDto.builder()
|
||||||
|
.name(flingEntity1.getName())
|
||||||
|
.authCode("new-secret")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
when(passwordEncoder.encode("new-secret")).thenReturn("new-secret-hash");
|
||||||
|
when(flingRepository.getOne(flingEntity1.getId())).thenReturn(flingEntity1);
|
||||||
|
FlingDto updatedEntity = flingService.replace(flingEntity1.getId(), flingDto);
|
||||||
|
|
||||||
|
verify(passwordEncoder, atLeast(1)).encode("new-secret");
|
||||||
|
assertThat(updatedEntity.getId(), is(flingEntity1.getId()));
|
||||||
|
assertThat(updatedEntity.getAuthCode(), is("new-secret-hash"));
|
||||||
|
assertThat(updatedEntity.getName(), is(flingEntity1.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getByShareId_flingDto() {
|
||||||
|
when(flingRepository.findByShareId("shareId2")).thenReturn(flingEntity2);
|
||||||
|
|
||||||
|
FlingDto foundFling = flingService.getByShareId("shareId2");
|
||||||
|
assertThat(foundFling.getShareId(), equalTo("shareId2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getByShareId_noFlingForShareId_throws() {
|
||||||
|
when(flingRepository.findByShareId(any(String.class))).thenReturn(null);
|
||||||
|
|
||||||
|
assertThrows(EntityNotFoundException.class, () -> flingService.getByShareId("doesNotExist"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void delete_deletesFromArchiveAndDb() throws IOException {
|
||||||
|
UUID testId = UUID.randomUUID();
|
||||||
|
flingService.delete(testId);
|
||||||
|
|
||||||
|
verify(archiveService).deleteFling(testId);
|
||||||
|
verify(flingRepository).deleteById(testId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getArtifacts_noArtifacts_emptySet() throws IOException {
|
||||||
|
UUID testId = UUID.randomUUID();
|
||||||
|
FlingEntity flingEntity = new FlingEntity();
|
||||||
|
flingEntity.setId(testId);
|
||||||
|
flingEntity.setArtifacts(null);
|
||||||
|
|
||||||
|
when(flingRepository.getOne(testId)).thenReturn(flingEntity);
|
||||||
|
|
||||||
|
assertThat(flingService.getArtifacts(testId), is(empty()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getArtifacts_flingWithArtifacts_artifactSet() throws Exception {
|
||||||
|
UUID artifactId = UUID.randomUUID();
|
||||||
|
ArtifactEntity artifactEntity = new ArtifactEntity();
|
||||||
|
artifactEntity.setId(artifactId);
|
||||||
|
|
||||||
|
UUID flingId = UUID.randomUUID();
|
||||||
|
FlingEntity flingEntity = new FlingEntity();
|
||||||
|
flingEntity.setId(flingId);
|
||||||
|
flingEntity.setArtifacts(Set.of(artifactEntity));
|
||||||
|
|
||||||
|
when(flingRepository.getOne(flingId)).thenReturn(flingEntity);
|
||||||
|
|
||||||
|
Set<ArtifactDto> artifacts = flingService.getArtifacts(flingId);
|
||||||
|
assertThat(artifacts, hasSize(1));
|
||||||
|
assertThat(artifacts.stream().map(ArtifactDto::getId).collect(Collectors.toSet()),
|
||||||
|
contains(artifactId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateAuthCode_codesMatch_true() {
|
||||||
|
when(flingRepository.getOne(flingEntity1.getId())).thenReturn(flingEntity1);
|
||||||
|
when(passwordEncoder.encode("authCode1")).thenReturn("testhash");
|
||||||
|
|
||||||
|
assertThat(flingService.validateAuthCode(flingEntity1.getId(), "authCode1"), is(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateAuthCode_codesDoNotMatch_false() {
|
||||||
|
when(flingRepository.getOne(flingEntity2.getId())).thenReturn(flingEntity2);
|
||||||
|
|
||||||
|
assertThat(flingService.validateAuthCode(flingEntity2.getId(), "authCode1"), is(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,305 @@
|
||||||
|
package net.friedl.fling.service.archive;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.hamcrest.Matchers.hasItem;
|
||||||
|
import static org.hamcrest.Matchers.not;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.nio.file.FileAlreadyExistsException;
|
||||||
|
import java.nio.file.FileVisitOption;
|
||||||
|
import java.nio.file.FileVisitResult;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.SimpleFileVisitor;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.TestConfiguration;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||||
|
import net.friedl.fling.persistence.entities.ArtifactEntity;
|
||||||
|
import net.friedl.fling.persistence.entities.FlingEntity;
|
||||||
|
import net.friedl.fling.persistence.repositories.ArtifactRepository;
|
||||||
|
import net.friedl.fling.service.archive.impl.FileSystemArchive;
|
||||||
|
|
||||||
|
@ExtendWith(SpringExtension.class)
|
||||||
|
public class FileSystemArchiveTest {
|
||||||
|
@Autowired
|
||||||
|
private FileSystemArchive fileSystemArchive;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private ArtifactRepository artifactRepository;
|
||||||
|
|
||||||
|
private FlingEntity flingEntity1;
|
||||||
|
|
||||||
|
private FlingEntity flingEntity2;
|
||||||
|
|
||||||
|
private ArtifactEntity artifactEntity1;
|
||||||
|
|
||||||
|
private ArtifactEntity artifactEntity2;
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
static Path tempDir;
|
||||||
|
|
||||||
|
@TestConfiguration
|
||||||
|
static class FlingServiceTestConfiguration {
|
||||||
|
@Bean
|
||||||
|
public FileSystemArchive fileSystemArchive(ArtifactRepository artifactRepository)
|
||||||
|
throws URISyntaxException {
|
||||||
|
FileSystemArchive fileSystemArchive = new FileSystemArchive(artifactRepository);
|
||||||
|
fileSystemArchive.setArchivePath(tempDir);
|
||||||
|
return fileSystemArchive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void beforeEach() throws IOException, URISyntaxException {
|
||||||
|
repopulateArchivePath();
|
||||||
|
setupTestEntites();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getArtifact_flingDiskForFlingIdDoesNotExist_throws() {
|
||||||
|
flingEntity1.setId(UUID.randomUUID());
|
||||||
|
artifactEntity1.setFling(flingEntity1);
|
||||||
|
when(artifactRepository.getOne(artifactEntity1.getId())).thenReturn(artifactEntity1);
|
||||||
|
|
||||||
|
assertThrows(IOException.class, () -> fileSystemArchive.getArtifact(artifactEntity1.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getArtifact_returnsArtifact() throws IOException {
|
||||||
|
when(artifactRepository.getOne(artifactEntity1.getId())).thenReturn(artifactEntity1);
|
||||||
|
|
||||||
|
InputStream expectedArtifact =
|
||||||
|
getClass().getClassLoader().getResourceAsStream("filesystem/artifacts/artifact1");
|
||||||
|
byte[] expectedArtifactData = expectedArtifact.readAllBytes();
|
||||||
|
expectedArtifact.close();
|
||||||
|
|
||||||
|
InputStream retrievedArtifact = fileSystemArchive.getArtifact(artifactEntity1.getId());
|
||||||
|
byte[] retrievedArtifactData = retrievedArtifact.readAllBytes();
|
||||||
|
retrievedArtifact.close();
|
||||||
|
|
||||||
|
assertThat(retrievedArtifactData, equalTo(expectedArtifactData));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getFling_doesNotExist_throws() {
|
||||||
|
assertThrows(IOException.class, () -> fileSystemArchive.getFling(UUID.randomUUID()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getFling_returnsFling() throws IOException {
|
||||||
|
UUID flingUUID = new UUID(0, 0);
|
||||||
|
InputStream expectedFling = getClass().getClassLoader()
|
||||||
|
.getResourceAsStream("filesystem/archive_path/" + flingUUID.toString() + ".zip");
|
||||||
|
byte[] expectedFlingData = expectedFling.readAllBytes();
|
||||||
|
expectedFling.close();
|
||||||
|
|
||||||
|
InputStream retrievedFling = fileSystemArchive.getFling(flingUUID);
|
||||||
|
byte[] retrievedFlingData = retrievedFling.readAllBytes();
|
||||||
|
retrievedFling.close();
|
||||||
|
|
||||||
|
assertThat(retrievedFlingData, equalTo(expectedFlingData));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deleteArtifact_setsArchivedFalse() throws IOException {
|
||||||
|
when(artifactRepository.getOne(artifactEntity1.getId())).thenReturn(artifactEntity1);
|
||||||
|
|
||||||
|
fileSystemArchive.deleteArtifact(artifactEntity1.getId());
|
||||||
|
|
||||||
|
assertThat(artifactEntity1.getArchived(), equalTo(false));
|
||||||
|
|
||||||
|
InputStream flingStream =
|
||||||
|
new FileInputStream(
|
||||||
|
tempDir.resolve(artifactEntity1.getFling().getId().toString() + ".zip").toFile());
|
||||||
|
ZipInputStream zis = new ZipInputStream(flingStream);
|
||||||
|
ZipEntry zipEntry;
|
||||||
|
while ((zipEntry = zis.getNextEntry()) != null) {
|
||||||
|
assertThat(zipEntry.getName(), not(equalTo(artifactEntity1.getPath().toString())));
|
||||||
|
zis.closeEntry();
|
||||||
|
}
|
||||||
|
zis.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deleteArtifact_deletesArtifactFromZipDisk() throws IOException {
|
||||||
|
when(artifactRepository.getOne(artifactEntity1.getId())).thenReturn(artifactEntity1);
|
||||||
|
|
||||||
|
fileSystemArchive.deleteArtifact(artifactEntity1.getId());
|
||||||
|
|
||||||
|
InputStream flingStream =
|
||||||
|
new FileInputStream(
|
||||||
|
tempDir.resolve(artifactEntity1.getFling().getId().toString() + ".zip").toFile());
|
||||||
|
ZipInputStream zis = new ZipInputStream(flingStream);
|
||||||
|
ZipEntry zipEntry;
|
||||||
|
while ((zipEntry = zis.getNextEntry()) != null) {
|
||||||
|
assertThat(zipEntry.getName(), not(equalTo(artifactEntity1.getPath().toString())));
|
||||||
|
zis.closeEntry();
|
||||||
|
}
|
||||||
|
zis.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void storeArtifact_setsArchivedTrue() throws IOException, URISyntaxException {
|
||||||
|
InputStream artifact2Stream = new FileInputStream(
|
||||||
|
new File(
|
||||||
|
getClass().getClassLoader().getResource("filesystem/artifacts/artifact2").toURI()));
|
||||||
|
when(artifactRepository.getOne(artifactEntity2.getId())).thenReturn(artifactEntity2);
|
||||||
|
|
||||||
|
fileSystemArchive.storeArtifact(artifactEntity2.getId(), artifact2Stream);
|
||||||
|
|
||||||
|
artifact2Stream.close();
|
||||||
|
|
||||||
|
assertThat(artifactEntity2.getArchived(), equalTo(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void storeArtifact_storesArtifactToFlingDisk() throws URISyntaxException, IOException {
|
||||||
|
InputStream artifact2Stream = new FileInputStream(
|
||||||
|
new File(
|
||||||
|
getClass().getClassLoader().getResource("filesystem/artifacts/artifact2").toURI()));
|
||||||
|
when(artifactRepository.getOne(artifactEntity2.getId())).thenReturn(artifactEntity2);
|
||||||
|
|
||||||
|
fileSystemArchive.storeArtifact(artifactEntity2.getId(), artifact2Stream);
|
||||||
|
|
||||||
|
artifact2Stream.close();
|
||||||
|
|
||||||
|
InputStream flingStream =
|
||||||
|
new FileInputStream(
|
||||||
|
tempDir.resolve(artifactEntity2.getFling().getId().toString() + ".zip").toFile());
|
||||||
|
ZipInputStream zis = new ZipInputStream(flingStream);
|
||||||
|
ZipEntry zipEntry;
|
||||||
|
List<String> diskEntries = new LinkedList<>();
|
||||||
|
while ((zipEntry = zis.getNextEntry()) != null) {
|
||||||
|
diskEntries.add(zipEntry.getName());
|
||||||
|
zis.closeEntry();
|
||||||
|
}
|
||||||
|
zis.close();
|
||||||
|
|
||||||
|
assertThat(diskEntries, hasItem(Path.of("/").relativize(artifactEntity2.getPath()).toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deleteFling_setsArchivedFalseForAllContainedArtifacts() throws IOException {
|
||||||
|
when(artifactRepository.findAllByFlingId(artifactEntity1.getFling().getId()))
|
||||||
|
.thenReturn(List.of(artifactEntity1));
|
||||||
|
|
||||||
|
fileSystemArchive.deleteFling(artifactEntity1.getFling().getId());
|
||||||
|
|
||||||
|
assertThat(artifactEntity1.getArchived(), equalTo(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deleteFling_deletesZipDisk() throws IOException {
|
||||||
|
assertThat(Files.exists(tempDir.resolve(artifactEntity1.getFling().getId() + ".zip")),
|
||||||
|
equalTo(true));
|
||||||
|
|
||||||
|
fileSystemArchive.deleteFling(artifactEntity1.getFling().getId());
|
||||||
|
|
||||||
|
assertThat(Files.exists(tempDir.resolve(artifactEntity1.getFling().getId() + ".zip")),
|
||||||
|
equalTo(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deleteFling_zipDiskNotFound_noThrow() throws IOException {
|
||||||
|
assertThat(Files.exists(tempDir.resolve(artifactEntity2.getFling().getId() + ".zip")),
|
||||||
|
equalTo(false));
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> fileSystemArchive.deleteFling(artifactEntity2.getFling().getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void repopulateArchivePath() throws IOException, URISyntaxException {
|
||||||
|
Files.walkFileTree(tempDir, new SimpleFileVisitor<Path>() {
|
||||||
|
@Override
|
||||||
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
||||||
|
throws IOException {
|
||||||
|
Files.delete(file);
|
||||||
|
return FileVisitResult.CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileVisitResult postVisitDirectory(Path dir, IOException e)
|
||||||
|
throws IOException {
|
||||||
|
if (e == null) {
|
||||||
|
Files.delete(dir);
|
||||||
|
return FileVisitResult.CONTINUE;
|
||||||
|
} else {
|
||||||
|
// directory iteration failed
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Path source =
|
||||||
|
Path.of(getClass().getClassLoader().getResource("filesystem/archive_path").toURI());
|
||||||
|
Path target = tempDir;
|
||||||
|
Files.walkFileTree(source, Set.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,
|
||||||
|
new SimpleFileVisitor<Path>() {
|
||||||
|
@Override
|
||||||
|
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
|
||||||
|
throws IOException {
|
||||||
|
Path targetdir = target.resolve(source.relativize(dir));
|
||||||
|
try {
|
||||||
|
Files.copy(dir, targetdir);
|
||||||
|
} catch (FileAlreadyExistsException e) {
|
||||||
|
if (!Files.isDirectory(targetdir))
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
return FileVisitResult.CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
||||||
|
throws IOException {
|
||||||
|
Files.copy(file, target.resolve(source.relativize(file)));
|
||||||
|
return FileVisitResult.CONTINUE;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupTestEntites() {
|
||||||
|
// Fling1/Artifact1
|
||||||
|
this.artifactEntity1 = new ArtifactEntity();
|
||||||
|
artifactEntity1.setId(UUID.randomUUID());
|
||||||
|
artifactEntity1.setPath(Path.of("artifact1"));
|
||||||
|
artifactEntity1.setArchived(true);
|
||||||
|
|
||||||
|
this.flingEntity1 = new FlingEntity();
|
||||||
|
flingEntity1.setId(new UUID(0, 0));
|
||||||
|
flingEntity1.setName("fling1");
|
||||||
|
|
||||||
|
artifactEntity1.setFling(flingEntity1);
|
||||||
|
|
||||||
|
|
||||||
|
// Fling2/Artifact2
|
||||||
|
this.artifactEntity2 = new ArtifactEntity();
|
||||||
|
artifactEntity2.setId(UUID.randomUUID());
|
||||||
|
artifactEntity2.setPath(Path.of("/", "/sub", "artifact2"));
|
||||||
|
artifactEntity2.setArchived(false);
|
||||||
|
|
||||||
|
this.flingEntity2 = new FlingEntity();
|
||||||
|
flingEntity2.setId(new UUID(1, 0));
|
||||||
|
flingEntity2.setName("fling2");
|
||||||
|
|
||||||
|
artifactEntity2.setFling(flingEntity2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
fling.security.jwt-expiration=18000
|
||||||
|
fling.security.admin-name=admin
|
||||||
|
fling.security.admin-password=123
|
Binary file not shown.
|
@ -0,0 +1 @@
|
||||||
|
artifact1 ok
|
|
@ -0,0 +1 @@
|
||||||
|
artifact2 ok
|
|
@ -10,13 +10,4 @@
|
||||||
<password>${env.NEXUS_PASSWORD}</password>
|
<password>${env.NEXUS_PASSWORD}</password>
|
||||||
</server>
|
</server>
|
||||||
</servers>
|
</servers>
|
||||||
|
|
||||||
<mirrors>
|
|
||||||
<mirror>
|
|
||||||
<!--This sends everything else to /public -->
|
|
||||||
<id>nexus</id>
|
|
||||||
<mirrorOf>central</mirrorOf>
|
|
||||||
<url>https://nexus.friedl.net/repository/maven-central/</url>
|
|
||||||
</mirror>
|
|
||||||
</mirrors>
|
|
||||||
</settings>
|
</settings>
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
REACT_APP_API=https://fling.friedl.net/api
|
REACT_APP_API=https://fling.friedl.net
|
||||||
REACT_APP_LOGLEVEL=warn
|
REACT_APP_LOGLEVEL=warn
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
REACT_APP_API=http://localhost:8080/api
|
REACT_APP_API=http://localhost:8080/
|
||||||
REACT_APP_LOGLEVEL=trace
|
REACT_APP_LOGLEVEL=trace
|
||||||
|
|
1
web/fling/.npmrc
Normal file
1
web/fling/.npmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
@fling:registry=https://nexus.friedl.net/repository/npm-private/
|
6658
web/fling/package-lock.json
generated
6658
web/fling/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -3,15 +3,17 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fling/flingclient": "0.1.0-snapshot",
|
||||||
"@testing-library/jest-dom": "^4.2.4",
|
"@testing-library/jest-dom": "^4.2.4",
|
||||||
"@testing-library/react": "^9.5.0",
|
"@testing-library/react": "^9.5.0",
|
||||||
"@testing-library/user-event": "^7.2.1",
|
"@testing-library/user-event": "^7.2.1",
|
||||||
"axios": "^0.19.2",
|
"axios": "^0.19.2",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
|
"immer": "^7.0.7",
|
||||||
"jwt-decode": "^2.2.0",
|
"jwt-decode": "^2.2.0",
|
||||||
"loglevel": "^1.6.8",
|
"loglevel": "^1.6.8",
|
||||||
"node-sass": "^4.14.0",
|
"node-sass": "^4.14.1",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
|
@ -19,6 +21,7 @@
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-scripts": "3.4.1",
|
"react-scripts": "3.4.1",
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
|
"redux-thunk": "^2.3.0",
|
||||||
"spectre.css": "^0.5.8"
|
"spectre.css": "^0.5.8"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
||||||
|
|
||||||
import { Switch, Route, Redirect } from "react-router-dom";
|
import { Switch, Route, Redirect } from "react-router-dom";
|
||||||
|
|
||||||
import request, {isOwner, isUser} from './util/request';
|
import jwt from './util/jwt.js';
|
||||||
|
|
||||||
import Login from './components/admin/Login';
|
import Login from './components/admin/Login';
|
||||||
import FlingAdmin from './components/admin/FlingAdmin';
|
import FlingAdmin from './components/admin/FlingAdmin';
|
||||||
|
@ -12,6 +12,19 @@ import Unlock from './components/user/Unlock';
|
||||||
import FlingUser from './components/user/FlingUser';
|
import FlingUser from './components/user/FlingUser';
|
||||||
import LandingPage from './components/LandingPage';
|
import LandingPage from './components/LandingPage';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Front routes, defaults to a 404 Page.
|
||||||
|
* Routes:
|
||||||
|
* - / : Landing page
|
||||||
|
* - /admin/login : A login page. Redirects with admin token upon successful
|
||||||
|
login
|
||||||
|
* - /admin : The fling administration page. Redirects to a login page if not
|
||||||
|
authenticated
|
||||||
|
* - /admin/[fling id]/* : Go directly to a fling (sub-)page. Redirects to a
|
||||||
|
login page if not authenticated
|
||||||
|
* - /unlock : A unlock page. Redirects with user token upon successful login.
|
||||||
|
* - /f/[shareId] : Opens a fling page for a user
|
||||||
|
*/
|
||||||
export default () => {
|
export default () => {
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
|
@ -19,7 +32,7 @@ export default () => {
|
||||||
|
|
||||||
<Route exact path="/admin/login" component={Login} />
|
<Route exact path="/admin/login" component={Login} />
|
||||||
<OwnerRoute exact path="/admin"><FlingAdmin /></OwnerRoute>
|
<OwnerRoute exact path="/admin"><FlingAdmin /></OwnerRoute>
|
||||||
<OwnerRoute path="/admin/:fling"><FlingAdmin /></OwnerRoute>
|
<OwnerRoute path="/admin/:flingId"><FlingAdmin /></OwnerRoute>
|
||||||
|
|
||||||
<Route exact path="/unlock" component={Unlock} />
|
<Route exact path="/unlock" component={Unlock} />
|
||||||
<UserRoute path="/f/:shareId"><FlingUser /></UserRoute>
|
<UserRoute path="/f/:shareId"><FlingUser /></UserRoute>
|
||||||
|
@ -29,36 +42,55 @@ export default () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A wrapper for <Route> that redirects to the login
|
/*
|
||||||
// screen if you're not yet authenticated.
|
* A wrapper for <Route> that redirects to the login screen if no admin
|
||||||
|
* authentication token was found.
|
||||||
|
*
|
||||||
|
* Note that the token check is purely client-side. It provides no actual
|
||||||
|
* protection! It is hence possible to reach the admin site with some small
|
||||||
|
* amount of trickery. Without a valid token no meaningful actions are possible
|
||||||
|
* on the admin page though.
|
||||||
|
*/
|
||||||
function OwnerRoute({ children, ...rest }) {
|
function OwnerRoute({ children, ...rest }) {
|
||||||
|
log.info(`Routing request for ${rest['path']}`);
|
||||||
return (
|
return (
|
||||||
<Route
|
<Route
|
||||||
{...rest}
|
{...rest}
|
||||||
render={({ location }) => {
|
render={({ location }) => {
|
||||||
log.info(request.defaults);
|
if (jwt.hasSubject("admin")) { return children; }
|
||||||
if(isOwner()) { return children; }
|
else {
|
||||||
else { return <Redirect to={{pathname: "/admin/login", state: {from: location}}} />; }
|
return <Redirect to={{
|
||||||
|
pathname: "/admin/login",
|
||||||
|
state: { from: location }
|
||||||
|
}} />;
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A wrapper for <Route> that redirects to the unlock
|
/* A wrapper for <Route> that redirects to the unlock screen if no authorized
|
||||||
// screen if the fling is protected
|
* token * was found.
|
||||||
|
*
|
||||||
|
* Note that the token check is purely client-side. It provides no actual
|
||||||
|
* protection! It is hence possible to reach the target site with some small
|
||||||
|
* amount of trickery. Without a valid token, no meaningful actions are possible
|
||||||
|
* on the target page though - this must be checked server side.
|
||||||
|
*/
|
||||||
function UserRoute({ children, ...rest }) {
|
function UserRoute({ children, ...rest }) {
|
||||||
|
log.debug(`Routing request for ${rest['path']}`);
|
||||||
return (
|
return (
|
||||||
<Route
|
<Route
|
||||||
{...rest}
|
{...rest}
|
||||||
render={({ match, location }) => {
|
render={({ match, location }) => {
|
||||||
log.info(request.defaults);
|
let state = { from: location, shareId: match.params.shareId };
|
||||||
log.info(match);
|
|
||||||
log.info(location);
|
|
||||||
let x = {from: location, shareId: match.params.shareId};
|
|
||||||
|
|
||||||
if(isOwner()) { return children; }
|
let authorized =
|
||||||
else if(isUser(match.params.shareId)) { return children; }
|
jwt.hasSubject("admin")
|
||||||
else { return <Redirect to={ {pathname: "/unlock", state: x} } />; }
|
|| ( jwt.hasSubject("user") && jwt.hasClaim("shareId", state['shareId']) );
|
||||||
|
|
||||||
|
if (authorized) { return children; }
|
||||||
|
else { return <Redirect to={{ pathname: "/unlock", state: state }} />; }
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -13,7 +13,6 @@ export default function LandingPage() {
|
||||||
|
|
||||||
function openFling(ev) {
|
function openFling(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
window.location = `/f/${shareId}`;
|
window.location = `/f/${shareId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default (props) => {
|
|
||||||
function renderError() {
|
|
||||||
return (
|
|
||||||
<div className="toast toast-error mb-2">
|
|
||||||
<button className="btn btn-clear float-right" onClick={props.clearErrors}></button>
|
|
||||||
<h5>Ooops!</h5>
|
|
||||||
<li>
|
|
||||||
{ props.errors.map( (err, idx) => <ul key={idx}>{err}</ul> ) }
|
|
||||||
</li>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{ props.errors.length > 0 && !props.below ? renderError() : "" }
|
|
||||||
{ props.children }
|
|
||||||
{ props.errors.length > 0 && props.below ? renderError() : "" }
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,13 +1,26 @@
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { retrieveFlings, setActiveFling } from "../../redux/actions";
|
||||||
|
|
||||||
import Navbar from './Navbar';
|
import Navbar from './Navbar';
|
||||||
import FlingList from './FlingList';
|
import FlingList from './FlingList';
|
||||||
import FlingContent from './FlingContent';
|
import FlingContent from './FlingContent';
|
||||||
|
|
||||||
import {useParams} from 'react-router-dom';
|
|
||||||
|
|
||||||
export default function FlingAdmin() {
|
export default function FlingAdmin() {
|
||||||
let { fling } = useParams();
|
const { flingId } = useParams();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(retrieveFlings());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (flingId) {
|
||||||
|
dispatch(setActiveFling(flingId));
|
||||||
|
}
|
||||||
|
}, [flingId, dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -15,8 +28,12 @@ export default function FlingAdmin() {
|
||||||
|
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="columns mt-2">
|
<div className="columns mt-2">
|
||||||
<div className="column col-sm-12 col-lg-3 col-2"> <FlingList activeFling={fling} /> </div>
|
<div className="column col-sm-12 col-lg-3 col-2">
|
||||||
<div className="column col-sm-12"><FlingContent activeFling={fling} /></div>
|
<FlingList />
|
||||||
|
</div>
|
||||||
|
<div className="column col-sm-12">
|
||||||
|
<FlingContent />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,24 +1,29 @@
|
||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import {artifactClient} from '../../util/flingclient';
|
import { ArtifactClient, FlingClient, AuthClient } from '../../util/fc';
|
||||||
|
import { prettifyTimestamp } from '../../util/fn';
|
||||||
|
|
||||||
function FlingArtifactControl(props) {
|
function FlingArtifactControl(props) {
|
||||||
let iframeContainer = useRef(null);
|
let iframeContainer = useRef(null);
|
||||||
|
const artifactClient = new ArtifactClient();
|
||||||
|
const authClient = new AuthClient();
|
||||||
|
|
||||||
function handleDelete(ev) {
|
function handleDelete(ev) {
|
||||||
artifactClient.deleteArtifact(props.artifact.id)
|
artifactClient.deleteArtifact(props.artifact.id)
|
||||||
.then(() => props.reloadArtifactsFn());
|
.then(() => props.reloadArtifactsFn());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function handleDownload(ev) {
|
function handleDownload(ev) {
|
||||||
artifactClient.downloadArtifact(props.artifact.id)
|
authClient.deriveToken({ singleUse: true })
|
||||||
.then(url => {
|
.then(token => {
|
||||||
// We need this iframe hack because with a regular href, while
|
// We need this iframe hack because with a regular href, while
|
||||||
// the browser downloads the file fine, it also reloads the page, hence
|
// the browser downloads the file fine, it also reloads the page, hence
|
||||||
// loosing all logs and state
|
// loosing all logs and state
|
||||||
let frame = document.createElement("iframe");
|
let frame = document.createElement("iframe");
|
||||||
|
let url = `${process.env.REACT_APP_API.replace(/\/+$/, '')}/api/artifacts/${props.artifact.id}/data?derivedToken=${token}`;
|
||||||
|
log.trace(`Generated download url: ${url}`);
|
||||||
frame.src = url;
|
frame.src = url;
|
||||||
iframeContainer.current.appendChild(frame);
|
iframeContainer.current.appendChild(frame);
|
||||||
});
|
});
|
||||||
|
@ -36,25 +41,12 @@ function FlingArtifactControl(props) {
|
||||||
|
|
||||||
function FlingArtifactRow(props) {
|
function FlingArtifactRow(props) {
|
||||||
let [hovered, setHovered] = useState(false);
|
let [hovered, setHovered] = useState(false);
|
||||||
function readableBytes(bytes) {
|
|
||||||
if(bytes <= 0) return "0 KB";
|
|
||||||
|
|
||||||
var i = Math.floor(Math.log(bytes) / Math.log(1024)),
|
|
||||||
sizes = ['Byte', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
||||||
|
|
||||||
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
function localizedUploadDate() {
|
|
||||||
let d = new Date(props.artifact.uploadTime);
|
|
||||||
return d.toLocaleDateString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={props.artifact.id} className="artifact-row" onMouseOver={() => setHovered(true)} onMouseOut={() => setHovered(false)}>
|
<tr key={props.artifact.id} className="artifact-row" onMouseOver={() => setHovered(true)} onMouseOut={() => setHovered(false)}>
|
||||||
<td>{props.artifact.name}</td>
|
<td>{props.artifact.path}</td>
|
||||||
<td>{localizedUploadDate()}</td>
|
<td>{prettifyTimestamp(props.artifact.creationTime, true)}</td>
|
||||||
<td>{readableBytes(props.artifact.size)}</td>
|
<td></td>
|
||||||
<td><FlingArtifactControl artifact={props.artifact} reloadArtifactsFn={props.reloadArtifactsFn} hidden={!hovered} /></td>
|
<td><FlingArtifactControl artifact={props.artifact} reloadArtifactsFn={props.reloadArtifactsFn} hidden={!hovered} /></td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
@ -68,9 +60,12 @@ function FlingInfo(props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FlingArtifacts(props) {
|
export default function FlingArtifacts() {
|
||||||
|
const flingClient = new FlingClient();
|
||||||
|
const activeFling = useSelector(state => state.flings.activeFling);
|
||||||
const [artifacts, setArtifacts] = useState([]);
|
const [artifacts, setArtifacts] = useState([]);
|
||||||
useEffect(getArtifacts, [props.activeFling]);
|
|
||||||
|
useEffect(getArtifacts, [activeFling]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -93,15 +88,15 @@ export default function FlingArtifacts(props) {
|
||||||
);
|
);
|
||||||
|
|
||||||
function getArtifacts() {
|
function getArtifacts() {
|
||||||
if (!props.activeFling) {
|
if (!activeFling) {
|
||||||
log.debug("No fling active. Not getting artifacts.");
|
log.debug("No fling active. Not getting artifacts.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug(`Fling ${props.activeFling} active. Getting artifacts.`);
|
log.debug(`Fling ${activeFling} active. Getting artifacts.`);
|
||||||
let artifacts = [];
|
let artifacts = [];
|
||||||
|
|
||||||
artifactClient.getArtifacts(props.activeFling)
|
flingClient.getArtifacts(activeFling.id)
|
||||||
.then(result => {
|
.then(result => {
|
||||||
log.debug(`Got ${result.length} artifacts`);
|
log.debug(`Got ${result.length} artifacts`);
|
||||||
for (let artifact of result) {
|
for (let artifact of result) {
|
||||||
|
|
|
@ -2,21 +2,37 @@ import log from 'loglevel';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Switch, Route, useLocation, Link } from "react-router-dom";
|
import { Switch, Route, useLocation, Link } from "react-router-dom";
|
||||||
|
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
import FlingArtifacts from './FlingArtifacts';
|
import FlingArtifacts from './FlingArtifacts';
|
||||||
import Upload from './Upload';
|
import Upload from './Upload';
|
||||||
import Settings from './Settings';
|
import Settings from './Settings';
|
||||||
|
|
||||||
export default function FlingContent(props) {
|
export default function FlingContent() {
|
||||||
let location = useLocation();
|
const location = useLocation();
|
||||||
|
const activeFling = useSelector(state => state.flings.activeFling);
|
||||||
|
|
||||||
|
function Empty() {
|
||||||
|
return (
|
||||||
|
<div className="empty">
|
||||||
|
<div className="empty-icon">
|
||||||
|
<i className="icon icon-search icon-2x"></i>
|
||||||
|
</div>
|
||||||
|
<p className="empty-title h5">No Fling selected</p>
|
||||||
|
<p className="empty-subtitle">Select a fling from the list</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Content() {
|
||||||
function path(tail) {
|
function path(tail) {
|
||||||
return `/admin/${props.activeFling}/${tail}`;
|
return `/admin/${activeFling.id}/${tail}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fling-content p-2">
|
<div className="fling-content p-2">
|
||||||
{log.info("FlingContent location ", location)}
|
{log.info("FlingContent location ", location)}
|
||||||
{log.info("FlingContent active fling ", props.activeFling)}
|
{log.info("FlingContent active fling ", activeFling)}
|
||||||
<ul className="tab tab-block mt-0">
|
<ul className="tab tab-block mt-0">
|
||||||
<li className={`tab-item ${location.pathname !== path("upload") && location.pathname !== path("settings") ? "active" : ""}`}>
|
<li className={`tab-item ${location.pathname !== path("upload") && location.pathname !== path("settings") ? "active" : ""}`}>
|
||||||
<Link to={path("files")}>Files</Link>
|
<Link to={path("files")}>Files</Link>
|
||||||
|
@ -31,12 +47,19 @@ export default function FlingContent(props) {
|
||||||
|
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/admin/:fling"><FlingArtifacts activeFling={props.activeFling} /></Route>
|
<Route exact path="/admin/:fling"><FlingArtifacts /></Route>
|
||||||
<Route path="/admin/:fling/files"><FlingArtifacts activeFling={props.activeFling} /></Route>
|
<Route path="/admin/:fling/files"><FlingArtifacts /></Route>
|
||||||
<Route path="/admin/:fling/upload"><Upload activeFling={props.activeFling} /></Route>
|
<Route path="/admin/:fling/upload"><Upload /></Route>
|
||||||
<Route path="/admin/:fling/settings"><Settings activeFling={props.activeFling} /></Route>
|
<Route path="/admin/:fling/settings"><Settings /></Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{ activeFling ? Content() : Empty() }
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -1,37 +1,19 @@
|
||||||
import log from 'loglevel';
|
import React from 'react';
|
||||||
import React, {useState, useEffect} from 'react';
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
import {flingClient} from '../../util/flingclient';
|
|
||||||
|
|
||||||
import FlingTile from './FlingTile';
|
import FlingTile from './FlingTile';
|
||||||
|
|
||||||
export default function FlingList(props) {
|
export default function FlingList() {
|
||||||
const [flings, setFlings] = useState([]);
|
const flings = useSelector((store) => store.flings.flings);
|
||||||
useEffect(() => { (async () => {
|
|
||||||
let flings = await flingClient.getFlings();
|
|
||||||
let newFlings = [];
|
|
||||||
|
|
||||||
for (let fling of flings) {
|
|
||||||
let flingTile = <FlingTile fling={fling}
|
|
||||||
key={fling.id}
|
|
||||||
refreshFlingListFn={refreshFlingList} />;
|
|
||||||
newFlings.push(flingTile);
|
|
||||||
}
|
|
||||||
setFlings(newFlings);
|
|
||||||
})(); } , []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panel">
|
<div className="panel">
|
||||||
{log.info(`Got active fling: ${props.activeFling}`)}
|
|
||||||
<div className="panel-header p-2">
|
<div className="panel-header p-2">
|
||||||
<h5>My Flings</h5>
|
<h5>My Flings</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="panel-body p-0">
|
<div className="panel-body p-0">
|
||||||
{flings}
|
{flings.map(fling => <FlingTile fling={fling} key={fling.id} />)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
async function refreshFlingList() {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,40 +3,13 @@ import React, {useRef} from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { NavLink } from "react-router-dom";
|
import { NavLink } from "react-router-dom";
|
||||||
|
|
||||||
import {flingClient} from '../../util/flingclient';
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
|
|
||||||
|
import { deleteFling } from "../../redux/actions";
|
||||||
|
|
||||||
function TileAction(props) {
|
function TileAction(props) {
|
||||||
let shareUrlRef = useRef(null);
|
let shareUrlRef = useRef(null);
|
||||||
|
const dispatch = useDispatch();
|
||||||
return(
|
|
||||||
<div className="tile-action dropdown">
|
|
||||||
<button className="btn btn-link btn dropdown-toggle" tabIndex="0">
|
|
||||||
<i className="icon icon-more-vert" />
|
|
||||||
</button>
|
|
||||||
<ul className="menu text-left">
|
|
||||||
<li className="menu-item input-group">
|
|
||||||
<div className="input-group">
|
|
||||||
<input type="text" ref={shareUrlRef} className="form-input input-sm input-share-id" readOnly value={props.fling.sharing.shareUrl} />
|
|
||||||
<span className="input-group-addon addon-sm input-group-addon-sm" onClick={copyShareUrl} ><i className="icon icon-copy" /></span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li className="menu-item">
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-switch">
|
|
||||||
<input type="checkbox" checked={props.fling.sharing.shared} onChange={toggleShared} />
|
|
||||||
<i className="form-icon" />
|
|
||||||
{props.fling.sharing.shared ? "Shared":"Private"}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li className="menu-item">
|
|
||||||
<button className="btn btn-link text-warning pl-0" onClick={deleteFling}>
|
|
||||||
<i className="icon icon-delete mr-1" /> Remove
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
function copyShareUrl() {
|
function copyShareUrl() {
|
||||||
shareUrlRef.current.focus();
|
shareUrlRef.current.focus();
|
||||||
|
@ -51,21 +24,49 @@ function TileAction(props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteFling() {
|
return (
|
||||||
await flingClient.deleteFling(props.fling.id);
|
<div className="tile-action dropdown">
|
||||||
await props.refreshFlingListFn();
|
<button className="btn btn-link btn dropdown-toggle" tabIndex="0">
|
||||||
}
|
<i className="icon icon-more-vert" />
|
||||||
|
</button>
|
||||||
|
<ul className="menu text-left">
|
||||||
|
<li className="menu-item input-group">
|
||||||
|
<div className="input-group">
|
||||||
|
<input type="text" ref={shareUrlRef}
|
||||||
|
className="form-input input-sm input-share-id" readOnly
|
||||||
|
value={props.fling.shareId} />
|
||||||
|
<span className="input-group-addon addon-sm input-group-addon-sm"
|
||||||
|
onClick={copyShareUrl}>
|
||||||
|
<i className="icon icon-copy" /></span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="menu-item">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-switch">
|
||||||
|
<input type="checkbox" disabled checked={props.fling.shared} />
|
||||||
|
<i className="form-icon" />
|
||||||
|
{props.fling.shared ? "Shared" : "Private"}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="menu-item">
|
||||||
|
<button className="btn btn-link text-warning pl-0"
|
||||||
|
onClick={ev => dispatch(deleteFling(props.fling.id))}>
|
||||||
|
<i className="icon icon-delete mr-1" /> Remove
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
async function toggleShared() {
|
|
||||||
await flingClient.putFling(props.fling.id, {"sharing": {"shared": !props.fling.sharing.shared}});
|
|
||||||
await props.refreshFlingListFn();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FlingTile(props) {
|
export default function FlingTile(props) {
|
||||||
|
const activeFling = useSelector((state) => state.flings.activeFling);
|
||||||
|
|
||||||
let tileClasses = classNames(
|
let tileClasses = classNames(
|
||||||
"tile", "tile-centered", "p-2", "c-hand",
|
"tile", "tile-centered", "p-2", "c-hand",
|
||||||
{"active": props.activeFling === props.fling.id}
|
{ "active": activeFling ? activeFling.id === props.fling.id : false }
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -74,10 +75,14 @@ export default function FlingTile(props) {
|
||||||
<div className="tile-content">
|
<div className="tile-content">
|
||||||
<NavLink to={`/admin/${props.fling.id}`}>
|
<NavLink to={`/admin/${props.fling.id}`}>
|
||||||
<div className="tile-title">{props.fling.name}</div>
|
<div className="tile-title">{props.fling.name}</div>
|
||||||
<small className="tile-subtitle text-gray">14MB · Public · 1 Jan, 2017</small>
|
<small className="tile-subtitle text-gray">
|
||||||
|
{`${props.fling.shared ? "Shared" : "Private"}` +
|
||||||
|
` · ${(new Date(props.fling.creationTime)).toLocaleDateString()}` +
|
||||||
|
` · ${props.fling.authCode ? "Protected" : "Unprotected"}`}
|
||||||
|
</small>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<TileAction fling={props.fling} refreshFlingListFn={props.refreshFlingListFn} />
|
<TileAction fling={props.fling} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,34 +2,47 @@ import log from 'loglevel';
|
||||||
import React, {useState, useEffect} from 'react';
|
import React, {useState, useEffect} from 'react';
|
||||||
import {useHistory, useLocation} from 'react-router-dom';
|
import {useHistory, useLocation} from 'react-router-dom';
|
||||||
|
|
||||||
import request, {setAuth} from '../../util/request';
|
import {fc, AuthClient} from '../../util/fc';
|
||||||
|
|
||||||
import Error from './Error';
|
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const [errors, setErrors] = useState([]);
|
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { from } = location.state || { from: { pathname: "/admin" } };
|
const { from } = location.state || { from: { pathname: "/admin" } };
|
||||||
|
|
||||||
useEffect(() => setAuth(null), []);
|
useEffect(() => {
|
||||||
|
sessionStorage.removeItem("token")
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSubmit(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
let authClient = new AuthClient();
|
||||||
|
let opt = {adminAuth: new fc.AdminAuth(username, password)};
|
||||||
|
|
||||||
|
authClient.authenticateOwner(opt)
|
||||||
|
.then(response => {
|
||||||
|
log.info("Login successful");
|
||||||
|
sessionStorage.setItem('token', response);
|
||||||
|
log.debug("Returning back to", from);
|
||||||
|
history.replace(from);
|
||||||
|
}).catch(log.error);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container-center">
|
<div className="container-center">
|
||||||
<div>
|
<div>
|
||||||
<Error errors={errors} clearErrors={clearErrors} >
|
|
||||||
<form className="login-form" onSubmit={handleSubmit}>
|
<form className="login-form" onSubmit={handleSubmit}>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label" htmlFor="username">Username</label>
|
<label className="form-label" htmlFor="username">Username</label>
|
||||||
<input className="form-input" id="username" name="username" type="text" placeholder="Username"
|
<input className="form-input" id="username" name="username" type="text" placeholder="Username"
|
||||||
value={username} onChange={handleChange} />
|
value={username} onChange={ev => setUsername(ev.currentTarget.value)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label" htmlFor="password">Password</label>
|
<label className="form-label" htmlFor="password">Password</label>
|
||||||
<input className="form-input" id="password" name="password" type="password" placeholder={"*".repeat(18)}
|
<input className="form-input" id="password" name="password" type="password" placeholder={"*".repeat(18)}
|
||||||
value={password} onChange={handleChange} />
|
value={password} onChange={ev => setPassword(ev.currentTarget.value)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="login-action-row">
|
<div className="login-action-row">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
|
@ -41,48 +54,10 @@ export default function Login() {
|
||||||
<button className="btn btn-primary" type="submit">Sign In</button>
|
<button className="btn btn-primary" type="submit">Sign In</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Error>
|
|
||||||
|
|
||||||
<p className="login-footer">Ready. Set. Fling.</p>
|
<p className="login-footer">Ready. Set. Fling.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleSubmit(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
request.post("/auth/owner", {'username': username, 'password': password})
|
|
||||||
.then(response => {
|
|
||||||
log.info("Logged in successfully");
|
|
||||||
setAuth(response.data);
|
|
||||||
history.replace(from);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
log.error(error);
|
|
||||||
let response = error.response;
|
|
||||||
response.data && response.data.message && setErrors( prev => [response.data.message, ...prev] );
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function handleChange(ev) {
|
|
||||||
let name = ev.target.name;
|
|
||||||
let val = ev.target.value;
|
|
||||||
|
|
||||||
switch(name) {
|
|
||||||
case "username":
|
|
||||||
setUsername(val);
|
|
||||||
break;
|
|
||||||
case "password":
|
|
||||||
setPassword(val);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
log.error(`Cannot handle change ${name}`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function clearErrors() {
|
|
||||||
setErrors([]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import {flingClient} from '../../util/flingclient';
|
import { FlingClient, fc } from '../../util/fc';
|
||||||
|
|
||||||
export default function New(props) {
|
export default function New(props) {
|
||||||
let defaultState = () => ({name: "", authCode: "",
|
let defaultState = () => ({
|
||||||
|
name: "", authCode: "",
|
||||||
sharing: { directDownload: true, allowUpload: false, shared: true, shareUrl: "" },
|
sharing: { directDownload: true, allowUpload: false, shared: true, shareUrl: "" },
|
||||||
expiration: {}});
|
expiration: {}
|
||||||
|
});
|
||||||
|
|
||||||
let [fling, setFling] = useState(defaultState());
|
let [fling, setFling] = useState(defaultState());
|
||||||
let [shareUrlUnique, setShareUrlUnique] = useState(true);
|
let [shareUrlUnique, setShareUrlUnique] = useState(true);
|
||||||
|
@ -62,18 +64,11 @@ export default function New(props) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const flingClient = new FlingClient();
|
||||||
flingClient.getFlingByShareId(ev.currentTarget.value)
|
flingClient.getFlingByShareId(ev.currentTarget.value)
|
||||||
.then(result => {
|
.then(result => setShareUrlUnique(false))
|
||||||
if(!result) {
|
.catch(error => error.status === 404 && setShareUrlUnique(true) )
|
||||||
setShareUrlUnique(true);
|
.finally(() => { s.shareUrl = value; f.sharing = s; setFling(f); });
|
||||||
} else {
|
|
||||||
setShareUrlUnique(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
s.shareUrl = value;
|
|
||||||
f.sharing = s;
|
|
||||||
setFling(f);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setName(ev) {
|
function setName(ev) {
|
||||||
|
@ -132,9 +127,31 @@ export default function New(props) {
|
||||||
function handleSubmit(ev) {
|
function handleSubmit(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
log.info("Creating new filing");
|
log.info("Creating new filing");
|
||||||
log.info(fling);
|
const flingClient = new FlingClient();
|
||||||
flingClient.postFling(fling);
|
|
||||||
handleClose();
|
let flingEntity = new fc.Fling(fling.name);
|
||||||
|
flingEntity.directDownload = fling.sharing.directDownload;
|
||||||
|
flingEntity.allowUpload = fling.sharing.allowUpload;
|
||||||
|
flingEntity.shared = fling.sharing.shared;
|
||||||
|
flingEntity.shareId = fling.sharing.shareUrl;
|
||||||
|
flingEntity.authCode = fling.authCode;
|
||||||
|
if (fling.expiration.type) {
|
||||||
|
switch (fling.expiration.type) {
|
||||||
|
case "time":
|
||||||
|
flingEntity.expirationTime = fling.expiration.value;
|
||||||
|
break;
|
||||||
|
case "clicks":
|
||||||
|
flingEntity.expirationClicks = fling.expiration.value;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.warn("Unknown expiration type");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flingClient.postFling({ fling: flingEntity })
|
||||||
|
.then(() => handleClose())
|
||||||
|
.catch(error => log.error(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,194 +1,181 @@
|
||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
import React, {useState, useEffect} from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import produce from 'immer';
|
||||||
|
|
||||||
import {flingClient} from '../../util/flingclient';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import { retrieveFlings } from "../../redux/actions";
|
||||||
|
import { FlingClient } from '../../util/fc';
|
||||||
|
|
||||||
export default function Settings(props) {
|
export default function Settings(props) {
|
||||||
let defaultState = () => ({name: "", authCode: "",
|
let flingClient = new FlingClient();
|
||||||
sharing: {directDownload: false, allowUpload: true, shared: true, shareUrl: ""},
|
let dispatch = useDispatch();
|
||||||
expiration: {}});
|
|
||||||
|
/**
|
||||||
|
* The active fling from the redux store. Treat this as immutable.
|
||||||
|
*/
|
||||||
|
let activeFling = useSelector(state => state.flings.activeFling);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deep clone the active fling from redux into a draft. Changes to the
|
||||||
|
* settings will be stored in the draft until saved and pushed to the
|
||||||
|
* backend. This in turn will synchronize back to the redux store.
|
||||||
|
*
|
||||||
|
* The draft, just as the activeFling, is of type Fling
|
||||||
|
*/
|
||||||
|
let [draft, setDraft] = useState(produce(activeFling, draft => draft));
|
||||||
|
|
||||||
let [fling, setFling] = useState(defaultState());
|
|
||||||
let [shareUrlUnique, setShareUrlUnique] = useState(true);
|
let [shareUrlUnique, setShareUrlUnique] = useState(true);
|
||||||
let [authCodeChangeable, setAuthCodeChangeable] = useState(false);
|
let [authCodeChangeable, setAuthCodeChangeable] = useState(false);
|
||||||
let [reload, setReload] = useState(true);
|
let [expirationType, setExpirationType] = useState(activeFling.expirationClicks
|
||||||
|
? "clicks"
|
||||||
|
: activeFling.expirationTime ? "time" : "never");
|
||||||
|
|
||||||
useEffect(() => {
|
/**
|
||||||
if(props.activeFling && reload) {
|
* Publishes the draft to the backend and refreshes the redux store
|
||||||
flingClient.getFling(props.activeFling)
|
*/
|
||||||
.then(result => {
|
function publishDraft() {
|
||||||
let f = {...fling, ...result};
|
flingClient.putFling(activeFling.id, { fling: draft })
|
||||||
let s = {...fling.sharing, ...result.sharing};
|
.then(
|
||||||
let e = {...fling.expiration, ...result.expiration};
|
success => {
|
||||||
|
log.info("Saved new settings {}", draft);
|
||||||
|
dispatch(retrieveFlings());
|
||||||
|
})
|
||||||
|
.catch(error => log.error(`Could not save new settings for ${activeFling.id}: `, error));
|
||||||
|
}
|
||||||
|
|
||||||
f.sharing = s;
|
/**
|
||||||
f.expiration = e;
|
* Resets the draft to a new clone of the active fling. All draft
|
||||||
setFling(f);
|
* modifications get lost.
|
||||||
|
*/
|
||||||
|
function resetDraft() {
|
||||||
|
setDraft(produce({}, draft => activeFling));
|
||||||
|
}
|
||||||
|
|
||||||
setAuthCodeChangeable(!f.authCode);
|
/**
|
||||||
setReload(false);
|
* A helper shim for persistent produce.
|
||||||
|
*
|
||||||
|
* Executes `fun` in immer.produce, hereby generating a new draft `newDraft`,
|
||||||
|
* and sets it into local state via `setDraft(newDraft)`
|
||||||
|
*/
|
||||||
|
let _pproduce = (fun) => (...args) => {
|
||||||
|
let x = produce(fun)(...args);
|
||||||
|
setDraft(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the sharing toggles to valid combinations depending on the changed
|
||||||
|
* setting and its new value.
|
||||||
|
*
|
||||||
|
* Creates a new draft and sets it into the local state.
|
||||||
|
*/
|
||||||
|
let toggleSharing = _pproduce((newDraft, setting, enabled) => {
|
||||||
|
switch (setting) {
|
||||||
|
case "direct-download":
|
||||||
|
if (enabled) {
|
||||||
|
newDraft.directDownload = true;
|
||||||
|
newDraft.shared = true;
|
||||||
|
newDraft.allowUpload = false;
|
||||||
|
} else {
|
||||||
|
newDraft.directDownload = false;
|
||||||
|
}
|
||||||
|
return newDraft;
|
||||||
|
case "allow-upload":
|
||||||
|
if (enabled) {
|
||||||
|
newDraft.allowUpload = true;
|
||||||
|
newDraft.shared = true;
|
||||||
|
newDraft.directDownload = false;
|
||||||
|
} else {
|
||||||
|
newDraft.allowUpload = false;
|
||||||
|
}
|
||||||
|
return newDraft;
|
||||||
|
case "shared":
|
||||||
|
if (!enabled) {
|
||||||
|
newDraft.allowUpload = false;
|
||||||
|
newDraft.directDownload = false;
|
||||||
|
newDraft.shared = false;
|
||||||
|
} else {
|
||||||
|
newDraft.shared = true;
|
||||||
|
}
|
||||||
|
return newDraft;
|
||||||
|
default:
|
||||||
|
log.warn("Unknown action");
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
})
|
||||||
|
/** Sets the Fling.name. Creates a new draft and sets it into the local state. */
|
||||||
|
let setName = _pproduce((newDraft, name) => { newDraft.name = name });
|
||||||
|
/** Sets the Fling.shareId. Creates a new draft and sets it into the local
|
||||||
|
* state. Sets `setShareUrlUnique`. */
|
||||||
|
let setShareId = _pproduce((newDraft, shareId) => {
|
||||||
|
newDraft.shareId = shareId;
|
||||||
|
|
||||||
|
flingClient.getFlingByShareId(shareId)
|
||||||
|
.then(result => shareId !== activeFling.shareId && setShareUrlUnique(false))
|
||||||
|
.catch(error => error.status === 404 && setShareUrlUnique(true));
|
||||||
});
|
});
|
||||||
}
|
/** Sets the Fling.authCode. Creates a new draft and sets it into the local state. */
|
||||||
}, [props.activeFling, reload, fling]);
|
let setAuthCode = _pproduce((newDraft, authCode) => {
|
||||||
|
|
||||||
function reloadSettings(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
setFling(defaultState());
|
|
||||||
setReload(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetAuthCode(ev) {
|
|
||||||
if(fling.authCode) {
|
|
||||||
let f = {...fling};
|
|
||||||
f.authCode = "";
|
|
||||||
setFling(f);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!ev.currentTarget.checked) {
|
|
||||||
setAuthCodeChangeable(true);
|
setAuthCodeChangeable(true);
|
||||||
}
|
if (!authCode) return newDraft;
|
||||||
}
|
newDraft.authCode = authCode
|
||||||
|
|
||||||
function toggleSharing(ev) {
|
|
||||||
let f = {...fling};
|
|
||||||
let s = {...fling.sharing};
|
|
||||||
|
|
||||||
if(ev.currentTarget.id === "direct-download") {
|
|
||||||
if(ev.currentTarget.checked) {
|
|
||||||
s.directDownload = true;
|
|
||||||
s.shared = true;
|
|
||||||
s.allowUpload = false;
|
|
||||||
} else {
|
|
||||||
s.directDownload = false;
|
|
||||||
}
|
|
||||||
} else if(ev.currentTarget.id === "allow-upload") {
|
|
||||||
if(ev.currentTarget.checked) {
|
|
||||||
s.allowUpload = true;
|
|
||||||
s.shared = true;
|
|
||||||
s.directDownload = false;
|
|
||||||
} else {
|
|
||||||
s.allowUpload = false;
|
|
||||||
}
|
|
||||||
} else if(ev.currentTarget.id === "shared") {
|
|
||||||
if(!ev.currentTarget.checked) {
|
|
||||||
s.allowUpload = s.directDownload = s.shared = false;
|
|
||||||
} else {
|
|
||||||
s.shared = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
f.sharing = s;
|
|
||||||
|
|
||||||
setFling(f);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setShareUrl(ev) {
|
|
||||||
let f = {...fling};
|
|
||||||
let s = {...fling.sharing}; //TODO: expiration is not cloned
|
|
||||||
let value = ev.currentTarget.value;
|
|
||||||
|
|
||||||
if(!value) {
|
|
||||||
setShareUrlUnique(false);
|
|
||||||
s.shareUrl = value;
|
|
||||||
f.sharing = s;
|
|
||||||
setFling(f);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
flingClient.getFlingByShareId(ev.currentTarget.value)
|
|
||||||
.then(result => {
|
|
||||||
if(!result) {
|
|
||||||
setShareUrlUnique(true);
|
|
||||||
} else if(props.activeFling === result.id) { // share url didn't change
|
|
||||||
setShareUrlUnique(true);
|
|
||||||
} else {
|
|
||||||
setShareUrlUnique(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
s.shareUrl = value;
|
|
||||||
f.sharing = s;
|
|
||||||
setFling(f);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let resetAuthCode = _pproduce((newDraft) => {
|
||||||
|
setAuthCodeChangeable(true);
|
||||||
|
newDraft.authCode = "";
|
||||||
|
return newDraft;
|
||||||
|
});
|
||||||
|
|
||||||
|
let setExpiration = _pproduce((newDraft, type, value) => {
|
||||||
|
setExpirationType(type)
|
||||||
|
switch (type) {
|
||||||
|
case "clicks":
|
||||||
|
newDraft.expirationTime = "";
|
||||||
|
newDraft.expirationClicks = value;
|
||||||
|
break;
|
||||||
|
case "time":
|
||||||
|
newDraft.expirationClicks = "";
|
||||||
|
newDraft.expirationTime = value;
|
||||||
|
break;
|
||||||
|
case "never":
|
||||||
|
newDraft.expirationClicks = "";
|
||||||
|
newDraft.expirationTime = "";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.error("Unknown expiration type");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
function setName(ev) {
|
|
||||||
let f = {...fling};
|
|
||||||
let value = ev.currentTarget.value;
|
|
||||||
|
|
||||||
f.name = value;
|
|
||||||
setFling(f);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setExpirationType(ev) {
|
|
||||||
let f = {...fling};
|
|
||||||
let e = {...fling.expiration}; //TODO: sharing is not cloned
|
|
||||||
let value = ev.currentTarget.value;
|
|
||||||
|
|
||||||
if(value === "never") {
|
|
||||||
e = {};
|
|
||||||
} else {
|
|
||||||
e.type = value;
|
|
||||||
e.value = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
f.expiration = e;
|
|
||||||
setFling(f);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setExpirationValue(ev) {
|
|
||||||
let f = {...fling};
|
|
||||||
let e = {...fling.expiration}; //TODO: sharing is not cloned
|
|
||||||
let value = e.type === "time" ? ev.currentTarget.valueAsNumber: ev.currentTarget.value;
|
|
||||||
|
|
||||||
e.value = value;
|
|
||||||
|
|
||||||
f.expiration = e;
|
|
||||||
setFling(f);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatExpirationTime() {
|
|
||||||
if (!fling.expiration || !fling.expiration.value || fling.expiration.type !== "time")
|
|
||||||
return "";
|
|
||||||
|
|
||||||
|
|
||||||
let date = new Date(fling.expiration.value);
|
let resetExpiration = (draft, type) => {
|
||||||
let fmt = date.toISOString().split("T")[0];
|
setExpiration(draft, type, "");
|
||||||
return fmt;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
function setAuthCode(ev) {
|
|
||||||
let f = {...fling};
|
|
||||||
let value = ev.currentTarget.value;
|
|
||||||
|
|
||||||
f.authCode = value;
|
|
||||||
setFling(f);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSubmit(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
log.info(fling);
|
|
||||||
flingClient.putFling(props.activeFling, fling);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="columns">
|
<div className="columns">
|
||||||
<div className="p-centered column col-xl-9 col-sm-12 col-6">
|
<div className="p-centered column col-xl-9 col-sm-12 col-6">
|
||||||
<form className="form-horizontal" onSubmit={handleSubmit}>
|
<form className="form-horizontal" onSubmit={(ev) => { ev.preventDefault(); publishDraft(); }}>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div className="col-3 col-sm-12">
|
<div className="col-3 col-sm-12">
|
||||||
<label className="form-label" htmlFor="input-name">Name</label>
|
<label className="form-label" htmlFor="input-name">Name</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-9 col-sm-12">
|
<div className="col-9 col-sm-12">
|
||||||
<input className="form-input" type="text" id="input-name" value={fling.name} onChange={setName}/>
|
<input className="form-input" type="text" id="input-name"
|
||||||
|
value={draft.name}
|
||||||
|
onChange={(ev) => setName(draft, ev.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div className="col-3 col-sm-12">
|
<div className="col-3 col-sm-12">
|
||||||
<label className="form-label" htmlFor="input-share-url">Share URL</label>
|
<label className="form-label" htmlFor="input-share-url">Share Id</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-9 col-sm-12">
|
<div className="col-9 col-sm-12">
|
||||||
<input className="form-input" type="text" id="input-share-url" value={fling.sharing.shareUrl} onChange={setShareUrl} />
|
<input className="form-input" type="text" id="input-share-url"
|
||||||
|
value={draft.shareId}
|
||||||
|
onChange={ev => setShareId(draft, ev.target.value)} />
|
||||||
<i className={`icon icon-cross text-error ${shareUrlUnique ? "d-hide" : "d-visible"}`} />
|
<i className={`icon icon-cross text-error ${shareUrlUnique ? "d-hide" : "d-visible"}`} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -199,13 +186,16 @@ export default function Settings(props) {
|
||||||
</div>
|
</div>
|
||||||
<div className="col-9 col-sm-12">
|
<div className="col-9 col-sm-12">
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
<input className={`form-input ${authCodeChangeable ? "d-visible":"d-hide"}`} type="text" readOnly={!authCodeChangeable} value={fling.authCode} onChange={setAuthCode} />
|
<input className={`form-input ${!draft.authCode || authCodeChangeable ? "d-visible" : "d-hide"}`} type="text"
|
||||||
|
value={draft.authCode}
|
||||||
|
onChange={ev => setAuthCode(draft, ev.target.value)} />
|
||||||
|
|
||||||
<label className="form-switch ml-2 popover popover-bottom">
|
<label className="form-switch ml-2 popover popover-bottom">
|
||||||
<input type="checkbox" checked={!!fling.authCode} onChange={resetAuthCode} />
|
<input type="checkbox" checked={!!draft.authCode} onChange={ev => resetAuthCode(draft)} />
|
||||||
<i className="form-icon" /> Protected
|
<i className="form-icon" /> Protected
|
||||||
<div className="popover-container card">
|
<div className="popover-container card">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
{fling.authCode ? "Click to reset passcode": "Set passcode to enable protection"}
|
{draft.authCode ? "Click to reset passcode" : "Set passcode to enable protection"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
@ -220,25 +210,27 @@ export default function Settings(props) {
|
||||||
</div>
|
</div>
|
||||||
<div className="col-9 col-sm-12">
|
<div className="col-9 col-sm-12">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<select className="form-select" value={fling.expiration.type} onChange={setExpirationType}>
|
<select className="form-select" value={expirationType} onChange={ev => resetExpiration(draft, ev.currentTarget.value)}>
|
||||||
<option value="never">Never</option>
|
<option value="never">Never</option>
|
||||||
<option value="time">Date</option>
|
<option value="time">Date</option>
|
||||||
<option value="clicks">Clicks</option>
|
<option value="clicks">Clicks</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={fling.expiration.type === "clicks" ? "d-visible": "d-hide"}>
|
<div className={expirationType === "clicks" ? "d-visible" : "d-hide"}>
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
<span className="input-group-addon">Expire after</span>
|
<span className="input-group-addon">Expire after</span>
|
||||||
<input className="form-input" type="number" value={fling.expiration.value || ""} onChange={setExpirationValue} />
|
<input className="form-input" type="number" value={draft.expirationClicks} onChange={ev => setExpiration(draft, "clicks", ev.target.value)} />
|
||||||
<span className="input-group-addon">Clicks</span>
|
<span className="input-group-addon">Clicks</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={fling.expiration.type === "time" ? "d-visible": "d-hide"}>
|
<div className={expirationType === "time" ? "d-visible" : "d-hide"}>
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
<span className="input-group-addon">Expire after</span>
|
<span className="input-group-addon">Expire after</span>
|
||||||
<input className="form-input" type="date" value={formatExpirationTime()} onChange={setExpirationValue} />
|
<input className="form-input" type="date"
|
||||||
|
value={draft.expirationTime ? (new Date(draft.expirationTime)).toISOString().split('T')[0]: ""}
|
||||||
|
onChange={ev => setExpiration(draft, "time", ev.target.valueAsNumber)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -251,22 +243,25 @@ export default function Settings(props) {
|
||||||
<div className="col-9 col-sm-12">
|
<div className="col-9 col-sm-12">
|
||||||
|
|
||||||
<label className="form-switch form-inline">
|
<label className="form-switch form-inline">
|
||||||
<input type="checkbox" id="shared" checked={fling.sharing.shared} onChange={toggleSharing}/>
|
<input type="checkbox" id="shared" checked={draft.shared}
|
||||||
|
onChange={ev => toggleSharing(draft, ev.target.id, ev.target.checked)} />
|
||||||
<i className="form-icon" /> Shared
|
<i className="form-icon" /> Shared
|
||||||
</label>
|
</label>
|
||||||
<label className="form-switch form-inline">
|
<label className="form-switch form-inline">
|
||||||
<input type="checkbox" id="allow-upload" checked={fling.sharing.allowUpload} onChange={toggleSharing}/>
|
<input type="checkbox" id="allow-upload" checked={draft.allowUpload}
|
||||||
|
onChange={ev => toggleSharing(draft, ev.target.id, ev.target.checked)} />
|
||||||
<i className="form-icon" /> Uploads
|
<i className="form-icon" /> Uploads
|
||||||
</label>
|
</label>
|
||||||
<label className="form-switch form-inline">
|
<label className="form-switch form-inline">
|
||||||
<input type="checkbox" id="direct-download" checked={fling.sharing.directDownload} onChange={toggleSharing}/>
|
<input type="checkbox" id="direct-download" checked={draft.directDownload}
|
||||||
|
onChange={ev => toggleSharing(draft, ev.target.id, ev.target.checked)} />
|
||||||
<i className="form-icon" /> Direct Download
|
<i className="form-icon" /> Direct Download
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="float-right">
|
<div className="float-right">
|
||||||
<button className="btn btn-secondary mr-2" onClick={reloadSettings}>Cancel</button>
|
<button className="btn btn-secondary mr-2" onClick={resetDraft}>Cancel</button>
|
||||||
<input type="submit" className="btn btn-primary" value="Save" />
|
<input type="submit" className="btn btn-primary" value="Save" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,49 +1,43 @@
|
||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
import {artifactClient} from '../../util/flingclient';
|
import { ArtifactClient, FlingClient, fc } from '../../util/fc';
|
||||||
|
import { prettifyBytes, prettifyTimestamp } from '../../util/fn';
|
||||||
|
|
||||||
import upload from '../resources/upload.svg';
|
import upload from '../resources/upload.svg';
|
||||||
import drop from '../resources/drop.svg';
|
import drop from '../resources/drop.svg';
|
||||||
|
|
||||||
|
export default function Upload() {
|
||||||
export default function Upload(props) {
|
|
||||||
let fileInputRef = useRef(null);
|
let fileInputRef = useRef(null);
|
||||||
let [files, setFiles] = useState([]);
|
let [files, setFiles] = useState([]);
|
||||||
let [dragging, setDragging] = useState(false);
|
let [dragging, setDragging] = useState(false);
|
||||||
let [dragCount, setDragCount] = useState(0);
|
let [dragCount, setDragCount] = useState(0);
|
||||||
|
|
||||||
|
const activeFling = useSelector(state => state.flings.activeFling);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// prevent browser from trying to open the file when drag event
|
// prevent browser from trying to open the file when drag event not
|
||||||
// not recognized properly
|
// recognized properly
|
||||||
window.addEventListener("dragover",function(e){
|
window.addEventListener("dragover", e => e.preventDefault(), false);
|
||||||
e.preventDefault();
|
window.addEventListener("drop", e => e.preventDefault(), false);
|
||||||
},false);
|
|
||||||
window.addEventListener("drop",function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
},false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function fileList() {
|
function fileList() {
|
||||||
function readableBytes(bytes) {
|
|
||||||
if(bytes <= 0) return "0 KB";
|
|
||||||
|
|
||||||
var i = Math.floor(Math.log(bytes) / Math.log(1024)),
|
|
||||||
sizes = ['Byte', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
||||||
|
|
||||||
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileList = [];
|
let fileList = [];
|
||||||
files.forEach((file, idx) => {
|
files.forEach((file, idx) => {
|
||||||
if (!file.uploaded) {
|
if (!file.uploaded) {
|
||||||
fileList.push(
|
fileList.push(
|
||||||
<div className="column col-6 col-md-12 mb-2">
|
<div key={idx} className="column col-6 col-md-12 mb-2">
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<i className="icon icon-cross float-right c-hand" onClick={ev => deleteFile(idx)}/>
|
<i className="icon icon-cross float-right c-hand"
|
||||||
|
onClick={ev => deleteFile(idx)} />
|
||||||
<div className="card-title h5">{file.name}</div>
|
<div className="card-title h5">{file.name}</div>
|
||||||
<div className="card-subtitle text-gray">{(new Date(file.lastModified)).toLocaleString()+", "+readableBytes(file.size)}</div>
|
<div className="card-subtitle text-gray">
|
||||||
|
{`${prettifyTimestamp(file.lastModified)}, ` +
|
||||||
|
`${prettifyBytes(file.size)}`}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -61,21 +55,12 @@ export default function Upload(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function totalSize() {
|
function totalSize() {
|
||||||
function readableBytes(bytes) {
|
|
||||||
if(bytes <= 0) return "0 KB";
|
|
||||||
|
|
||||||
var i = Math.floor(Math.log(bytes) / Math.log(1024)),
|
|
||||||
sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
||||||
|
|
||||||
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
let totalSize = 0;
|
let totalSize = 0;
|
||||||
for (let file of files) {
|
for (let file of files) {
|
||||||
totalSize += file.size;
|
totalSize += file.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
return readableBytes(totalSize);
|
return prettifyBytes(totalSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClick(ev) {
|
function handleClick(ev) {
|
||||||
|
@ -152,9 +137,15 @@ export default function Upload(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleUpload() {
|
function handleUpload() {
|
||||||
|
const flingClient = new FlingClient();
|
||||||
|
const artifactClient = new ArtifactClient();
|
||||||
|
|
||||||
files.forEach((file, idx) => {
|
files.forEach((file, idx) => {
|
||||||
artifactClient.postArtifact(props.activeFling, file)
|
let artifact = new fc.Artifact(file.name)
|
||||||
.then(response => {
|
|
||||||
|
flingClient.postArtifact(activeFling.id, { artifact: artifact })
|
||||||
|
.then(artifact => {
|
||||||
|
artifactClient.uploadArtifactData(artifact.id, { body: file });
|
||||||
setFileUploaded(idx);
|
setFileUploaded(idx);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,27 +1,38 @@
|
||||||
|
import log from 'loglevel';
|
||||||
import React, { useRef, useState, useEffect } from 'react';
|
import React, { useRef, useState, useEffect } from 'react';
|
||||||
|
|
||||||
import {flingClient} from '../../util/flingclient';
|
import { AuthClient } from '../../util/fc';
|
||||||
|
|
||||||
export default function FlingUser(props) {
|
export default function FlingUser(props) {
|
||||||
let iframeContainer = useRef(null);
|
let iframeContainer = useRef(null);
|
||||||
let [packaging, setPackaging] = useState(true);
|
let [packaging, setPackaging] = useState(true);
|
||||||
|
let [done, setDone] = useState(false);
|
||||||
let [waitingMessage, setWaitingMessage] = useState("");
|
let [waitingMessage, setWaitingMessage] = useState("");
|
||||||
let [downloadUrl, setDownloadUrl] = useState("");
|
let [downloadUrl, setDownloadUrl] = useState("");
|
||||||
|
|
||||||
useEffect(handleDownload, []);
|
useEffect(handleDownload, []);
|
||||||
|
|
||||||
function handleDownload() {
|
function handleDownload() {
|
||||||
flingClient.packageFling(props.fling.id)
|
let authClient = new AuthClient();
|
||||||
.then(downloadUrl => {
|
authClient.deriveToken({ singleUse: true })
|
||||||
|
.then(token => {
|
||||||
|
let url = `${process.env.REACT_APP_API.replace(/\/+$/, '')}/api/fling/${props.fling.id}/data?derivedToken=${token}`;
|
||||||
|
log.trace(`Generated download url for link: ${url}`);
|
||||||
|
setDownloadUrl(url);
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
authClient.deriveToken({ singleUse: true })
|
||||||
|
.then(token => {
|
||||||
setPackaging(false);
|
setPackaging(false);
|
||||||
// We need this iframe hack because with a regular href, while
|
// We need this iframe hack because with a regular href, while
|
||||||
// the browser downloads the file fine, it also reloads the page, hence
|
// the browser downloads the file fine, it also reloads the page, hence
|
||||||
// loosing all logs and state
|
// loosing all logs and state
|
||||||
let frame = document.createElement("iframe");
|
let frame = document.createElement("iframe");
|
||||||
frame.src = downloadUrl;
|
let url = `${process.env.REACT_APP_API.replace(/\/+$/, '')}/api/fling/${props.fling.id}/data?derivedToken=${token}`;
|
||||||
|
log.trace(`Generated download url: ${url}`);
|
||||||
|
frame.src = url;
|
||||||
iframeContainer.current.appendChild(frame);
|
iframeContainer.current.appendChild(frame);
|
||||||
setDownloadUrl(downloadUrl);
|
}));
|
||||||
});
|
|
||||||
|
|
||||||
let randMsg = ["Please stay patient...",
|
let randMsg = ["Please stay patient...",
|
||||||
"Your download will be ready soon...",
|
"Your download will be ready soon...",
|
||||||
|
@ -30,6 +41,15 @@ export default function FlingUser(props) {
|
||||||
setInterval(() => setWaitingMessage(randMsg[Math.floor(Math.random() * randMsg.length)]), 10000);
|
setInterval(() => setWaitingMessage(randMsg[Math.floor(Math.random() * randMsg.length)]), 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function invalidateLink(ev) {
|
||||||
|
setDone(true);
|
||||||
|
window.location.href = downloadUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadPage(ev) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="container-center">
|
<div className="container-center">
|
||||||
|
@ -39,10 +59,18 @@ export default function FlingUser(props) {
|
||||||
? <><div className="loading loading-lg" />
|
? <><div className="loading loading-lg" />
|
||||||
{waitingMessage ? waitingMessage : "Packaging up your files..."}
|
{waitingMessage ? waitingMessage : "Packaging up your files..."}
|
||||||
</>
|
</>
|
||||||
: <>
|
: !done
|
||||||
|
? <>
|
||||||
<h5>Your download is <span className="text-primary">ready!</span></h5>
|
<h5>Your download is <span className="text-primary">ready!</span></h5>
|
||||||
<i className="icon icon-check icon-2x text-primary" /><br />
|
<i className="icon icon-check icon-2x text-primary" /><br />
|
||||||
<span className="text-dark">Download doesn't start? <br/><a href={downloadUrl}>Click here</a></span>
|
<span className="text-dark">Download doesn't start? <br />
|
||||||
|
<button className="btn btn-link" onClick={invalidateLink}>Click here</button></span>
|
||||||
|
</>
|
||||||
|
: <>
|
||||||
|
<h5>Thanks for <span className="text-primary">downloading!</span></h5>
|
||||||
|
<i className="icon icon-check icon-2x text-primary" /><br />
|
||||||
|
<span className="text-dark">Want to download again? <br />
|
||||||
|
<button className="btn btn-link" onClick={reloadPage}>Reload page</button></span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
import React, {useState} from 'react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import log from 'loglevel';
|
|
||||||
|
|
||||||
export default (props) => {
|
|
||||||
function renderError() {
|
|
||||||
return (
|
|
||||||
<div className="toast toast-error mb-2">
|
|
||||||
<button className="btn btn-clear float-right" onClick={props.clearErrors}></button>
|
|
||||||
<h5>Ooops!</h5>
|
|
||||||
<li>
|
|
||||||
{ props.errors.map( (err, idx) => <ul key={idx}>{err}</ul> ) }
|
|
||||||
</li>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{ props.errors.length > 0 && !props.below ? renderError() : "" }
|
|
||||||
{ props.children }
|
|
||||||
{ props.errors.length > 0 && props.below ? renderError() : "" }
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -2,7 +2,7 @@ import React, {useState, useEffect} from 'react';
|
||||||
|
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import {flingClient} from '../../util/flingclient';
|
import { FlingClient } from '../../util/fc';
|
||||||
|
|
||||||
import DirectDownload from './DirectDownload';
|
import DirectDownload from './DirectDownload';
|
||||||
import FlingUserList from './FlingUserList';
|
import FlingUserList from './FlingUserList';
|
||||||
|
@ -10,17 +10,27 @@ import FlingUserList from './FlingUserList';
|
||||||
export default function FlingUser() {
|
export default function FlingUser() {
|
||||||
let { shareId } = useParams();
|
let { shareId } = useParams();
|
||||||
let [fling, setFling] = useState({});
|
let [fling, setFling] = useState({});
|
||||||
|
let [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let flingClient = new FlingClient();
|
||||||
flingClient.getFlingByShareId(shareId)
|
flingClient.getFlingByShareId(shareId)
|
||||||
.then(f => setFling(f));
|
.then(f => {
|
||||||
|
setFling(f);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
}, [shareId]);
|
}, [shareId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
{fling.sharing && fling.sharing.directDownload
|
{loading
|
||||||
|
? <div></div>
|
||||||
|
: <div>
|
||||||
|
{fling.shared && fling.directDownload
|
||||||
? <DirectDownload fling={fling} />
|
? <DirectDownload fling={fling} />
|
||||||
: <FlingUserList fling={fling} />}
|
: <FlingUserList fling={fling} />}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,8 @@ import React, {useState, useEffect, useRef} from 'react';
|
||||||
|
|
||||||
import { Switch, Route, useLocation, Link } from "react-router-dom";
|
import { Switch, Route, useLocation, Link } from "react-router-dom";
|
||||||
|
|
||||||
import {flingClient, artifactClient} from '../../util/flingclient';
|
import { FlingClient, AuthClient, ArtifactClient, fc } from '../../util/fc';
|
||||||
|
import { prettifyTimestamp, prettifyBytes } from '../../util/fn';
|
||||||
|
|
||||||
import upload from '../resources/upload.svg';
|
import upload from '../resources/upload.svg';
|
||||||
import drop from '../resources/drop.svg';
|
import drop from '../resources/drop.svg';
|
||||||
|
@ -14,37 +15,25 @@ function Artifacts(props) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!props.fling) return;
|
if (!props.fling) return;
|
||||||
|
|
||||||
artifactClient.getArtifacts(props.fling.id)
|
let flingClient = new FlingClient();
|
||||||
.then((artifacts) => setArtifacts(artifacts));
|
flingClient.getArtifacts(props.fling.id)
|
||||||
|
.then(artifacts => setArtifacts(artifacts));
|
||||||
}, [props.fling]);
|
}, [props.fling]);
|
||||||
|
|
||||||
function renderArtifact(artifact) {
|
function renderArtifact(artifact) {
|
||||||
function readableBytes(bytes) {
|
|
||||||
if(bytes <= 0) return "0 KB";
|
|
||||||
|
|
||||||
var i = Math.floor(Math.log(bytes) / Math.log(1024)),
|
|
||||||
sizes = ['Byte', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
||||||
|
|
||||||
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
function localizedDate(t) {
|
|
||||||
let d = new Date(t);
|
|
||||||
return d.toLocaleDateString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="user-list-artifact">
|
<div className="user-list-artifact">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="columns">
|
<div className="columns">
|
||||||
<div className="column col-8 col-sm-12">
|
<div className="column col-8 col-sm-12">
|
||||||
{artifact.name}<br />
|
{artifact.path}<br />
|
||||||
</div>
|
</div>
|
||||||
<div className="column col-2 col-sm-6">
|
<div className="column col-2 col-sm-6">
|
||||||
<div className="text-gray">{readableBytes(artifact.size)}</div>
|
<div className="text-gray"></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="column col-2 col-sm-6">
|
<div className="column col-2 col-sm-6">
|
||||||
<div className="text-gray float-right">{localizedDate(artifact.uploadTime)}</div>
|
<div className="text-gray float-right">{prettifyTimestamp(artifact.creationTime)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -77,15 +66,6 @@ function Upload(props) {
|
||||||
});
|
});
|
||||||
|
|
||||||
function fileList() {
|
function fileList() {
|
||||||
function readableBytes(bytes) {
|
|
||||||
if(bytes <= 0) return "0 KB";
|
|
||||||
|
|
||||||
var i = Math.floor(Math.log(bytes) / Math.log(1024)),
|
|
||||||
sizes = ['Byte', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
||||||
|
|
||||||
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileList = [];
|
let fileList = [];
|
||||||
files.forEach((file, idx) => {
|
files.forEach((file, idx) => {
|
||||||
if (!file.uploaded) {
|
if (!file.uploaded) {
|
||||||
|
@ -95,7 +75,7 @@ function Upload(props) {
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<i className="icon icon-cross float-right c-hand" onClick={ev => deleteFile(idx)} />
|
<i className="icon icon-cross float-right c-hand" onClick={ev => deleteFile(idx)} />
|
||||||
<div className="card-title h5">{file.name}</div>
|
<div className="card-title h5">{file.name}</div>
|
||||||
<div className="card-subtitle text-gray">{(new Date(file.lastModified)).toLocaleString()+", "+readableBytes(file.size)}</div>
|
<div className="card-subtitle text-gray">{(new Date(file.lastModified)).toLocaleString() + ", " + prettifyBytes(file.size)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -113,21 +93,12 @@ function Upload(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function totalSize() {
|
function totalSize() {
|
||||||
function readableBytes(bytes) {
|
|
||||||
if(bytes <= 0) return "0 KB";
|
|
||||||
|
|
||||||
var i = Math.floor(Math.log(bytes) / Math.log(1024)),
|
|
||||||
sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
||||||
|
|
||||||
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
let totalSize = 0;
|
let totalSize = 0;
|
||||||
for (let file of files) {
|
for (let file of files) {
|
||||||
totalSize += file.size;
|
totalSize += file.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
return readableBytes(totalSize);
|
return prettifyBytes(totalSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClick(ev) {
|
function handleClick(ev) {
|
||||||
|
@ -204,9 +175,15 @@ function Upload(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleUpload() {
|
function handleUpload() {
|
||||||
|
const flingClient = new FlingClient();
|
||||||
|
const artifactClient = new ArtifactClient();
|
||||||
|
|
||||||
files.forEach((file, idx) => {
|
files.forEach((file, idx) => {
|
||||||
artifactClient.postArtifact(props.fling.id, file)
|
let artifact = new fc.Artifact(file.name)
|
||||||
.then(response => {
|
|
||||||
|
flingClient.postArtifact(props.fling.id, { artifact: artifact })
|
||||||
|
.then(artifact => {
|
||||||
|
artifactClient.uploadArtifactData(artifact.id, { body: file });
|
||||||
setFileUploaded(idx);
|
setFileUploaded(idx);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -275,34 +252,13 @@ export default function FlingUserList(props) {
|
||||||
let [infoText, setInfoText] = useState("");
|
let [infoText, setInfoText] = useState("");
|
||||||
let [inProgress, setInProgress] = useState(false);
|
let [inProgress, setInProgress] = useState(false);
|
||||||
|
|
||||||
useEffect((flingId) => {
|
useEffect(() => {
|
||||||
if(!flingId) return;
|
if (!props.fling.id) return;
|
||||||
|
|
||||||
function readableBytes(bytes) {
|
let flingClient = new FlingClient();
|
||||||
if(bytes <= 0) return "0 KB";
|
flingClient.getArtifacts(props.fling.id)
|
||||||
|
|
||||||
var i = Math.floor(Math.log(bytes) / Math.log(1024)),
|
|
||||||
sizes = ['Byte', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
||||||
|
|
||||||
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
function localizedDate(t) {
|
|
||||||
let d = new Date(t);
|
|
||||||
return d.toLocaleDateString();
|
|
||||||
}
|
|
||||||
|
|
||||||
artifactClient.getArtifacts(flingId)
|
|
||||||
.then((artifacts) => {
|
.then((artifacts) => {
|
||||||
let totalSize = 0;
|
setInfoText(`${prettifyTimestamp(props.fling.creationTime)} - ${artifacts.length} files`);
|
||||||
let countArtifacts = 0;
|
|
||||||
|
|
||||||
for(let artifact of artifacts) {
|
|
||||||
totalSize += artifact.size;
|
|
||||||
countArtifacts++;
|
|
||||||
}
|
|
||||||
|
|
||||||
setInfoText(`${localizedDate(props.fling.creationTime)} - ${countArtifacts} files - ${readableBytes(totalSize)}`);
|
|
||||||
});
|
});
|
||||||
}, [props.fling.id, props.fling.creationTime]);
|
}, [props.fling.id, props.fling.creationTime]);
|
||||||
|
|
||||||
|
@ -310,22 +266,24 @@ export default function FlingUserList(props) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
setInProgress(true);
|
setInProgress(true);
|
||||||
|
let authClient = new AuthClient();
|
||||||
flingClient.packageFling(props.fling.id)
|
authClient.deriveToken({ singleUse: true })
|
||||||
.then(downloadUrl => {
|
.then(token => {
|
||||||
// We need this iframe hack because with a regular href, while
|
// We need this iframe hack because with a regular href, while
|
||||||
// the browser downloads the file fine, it also reloads the page, hence
|
// the browser downloads the file fine, it also reloads the page, hence
|
||||||
// loosing all logs and state
|
// loosing all logs and state
|
||||||
let frame = document.createElement("iframe");
|
let frame = document.createElement("iframe");
|
||||||
frame.src = downloadUrl;
|
let url = `${process.env.REACT_APP_API.replace(/\/+$/, '')}/api/fling/${props.fling.id}/data?derivedToken=${token}`;
|
||||||
iframeContainer.current.appendChild(frame);
|
log.trace(`Generated download url: ${url}`);
|
||||||
|
frame.src = url;
|
||||||
setInProgress(false);
|
setInProgress(false);
|
||||||
|
iframeContainer.current.appendChild(frame);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function path(tail) {
|
function path(tail) {
|
||||||
if(props.fling && props.fling.sharing) {
|
if (props.fling && props.fling.shareId) {
|
||||||
return `/f/${props.fling.sharing.shareUrl}/${tail}`;
|
return `/f/${props.fling.shareId}/${tail}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
|
@ -345,14 +303,17 @@ export default function FlingUserList(props) {
|
||||||
<li className={`tab-item ${location.pathname !== path("upload") ? "active" : ""}`}>
|
<li className={`tab-item ${location.pathname !== path("upload") ? "active" : ""}`}>
|
||||||
<Link to={path("files")}>Files</Link>
|
<Link to={path("files")}>Files</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className={`tab-item ${location.pathname === path("upload") ? "active": ""}`}>
|
{ props.fling.allowUpload
|
||||||
|
? <li className={`tab-item ${location.pathname === path("upload") ? "active" : ""}`}>
|
||||||
<Link to={path("upload")}>Upload</Link>
|
<Link to={path("upload")}>Upload</Link>
|
||||||
</li>
|
</li>
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
|
|
||||||
<li className="tab-item tab-action">
|
<li className="tab-item tab-action">
|
||||||
<div className="card-title">
|
<div className="card-title">
|
||||||
{inProgress
|
{inProgress
|
||||||
? <button className="m-2 btn btn-xs btn-secondary float-right user-list-loading" disabled="true"
|
? <button className="m-2 btn btn-xs btn-secondary float-right user-list-loading" disabled
|
||||||
onClick={(ev) => ev.preventDefault()}>
|
onClick={(ev) => ev.preventDefault()}>
|
||||||
<div className="loading" /> Packaging
|
<div className="loading" /> Packaging
|
||||||
</button>
|
</button>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue