This commit is contained in:
parent
0c1fe8efce
commit
41f2a22f3d
21 changed files with 365 additions and 156 deletions
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}")
|
||||
|
||||
#
|
39
examples/querysheet.http
Normal file
39
examples/querysheet.http
Normal file
|
@ -0,0 +1,39 @@
|
|||
######################################
|
||||
# Fling Querysheet for restclient.el #
|
||||
######################################
|
||||
|
||||
# Authenticate as user
|
||||
POST http://localhost:8080/api/auth/user
|
||||
{"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)))
|
||||
|
||||
# Get all flings
|
||||
GET http://localhost:8080/api/fling
|
||||
Authorization: Bearer :token
|
||||
|
||||
#
|
||||
:flingId = dfc208a3-5924-43b4-aa6a-c263541dca5e
|
||||
|
||||
# Get one fling
|
||||
GET http://localhost:8080/api/fling/:flingId
|
||||
:token
|
||||
|
||||
# Get all artifacts
|
||||
GET http://localhost:8080/api/fling/:flingId/artifacts
|
||||
:token
|
||||
|
||||
#
|
||||
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))))
|
16
web/fling/package-lock.json
generated
16
web/fling/package-lock.json
generated
|
@ -6740,9 +6740,9 @@
|
|||
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg=="
|
||||
},
|
||||
"immer": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz",
|
||||
"integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg=="
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-7.0.5.tgz",
|
||||
"integrity": "sha512-TtRAKZyuqld2eYjvWgXISLJ0ZlOl1OOTzRmrmiY8SlB0dnAhZ1OiykIDL5KDFNaPHDXiLfGQFNJGtet8z8AEmg=="
|
||||
},
|
||||
"import-cwd": {
|
||||
"version": "2.1.0",
|
||||
|
@ -11512,6 +11512,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"immer": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz",
|
||||
"integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg=="
|
||||
},
|
||||
"inquirer": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz",
|
||||
|
@ -11922,6 +11927,11 @@
|
|||
"symbol-observable": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"redux-thunk": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
|
||||
"integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw=="
|
||||
},
|
||||
"regenerate": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"axios": "^0.19.2",
|
||||
"classnames": "^2.2.6",
|
||||
"core-js": "^3.6.5",
|
||||
"immer": "^7.0.5",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"loglevel": "^1.6.8",
|
||||
"node-sass": "^4.14.1",
|
||||
|
@ -20,6 +21,7 @@
|
|||
"react-router-dom": "^5.1.2",
|
||||
"react-scripts": "3.4.1",
|
||||
"redux": "^4.0.5",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"spectre.css": "^0.5.8"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
|
@ -12,6 +12,19 @@ import Unlock from './components/user/Unlock';
|
|||
import FlingUser from './components/user/FlingUser';
|
||||
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 () => {
|
||||
return (
|
||||
<Switch>
|
||||
|
@ -19,7 +32,7 @@ export default () => {
|
|||
|
||||
<Route exact path="/admin/login" component={Login} />
|
||||
<OwnerRoute exact path="/admin"><FlingAdmin /></OwnerRoute>
|
||||
<OwnerRoute path="/admin/:fling"><FlingAdmin /></OwnerRoute>
|
||||
<OwnerRoute path="/admin/:flingId"><FlingAdmin /></OwnerRoute>
|
||||
|
||||
<Route exact path="/unlock" component={Unlock} />
|
||||
<UserRoute path="/f/:shareId"><FlingUser /></UserRoute>
|
||||
|
@ -45,14 +58,19 @@ function OwnerRoute({ children, ...rest }) {
|
|||
{...rest}
|
||||
render={({ location }) => {
|
||||
if (jwt.hasSubject("admin")) { return children; }
|
||||
else { return <Redirect to={{pathname: "/admin/login", state: {from: location}}} />; }
|
||||
else {
|
||||
return <Redirect to={{
|
||||
pathname: "/admin/login",
|
||||
state: { from: location }
|
||||
}} />;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/* A wrapper for <Route> that redirects to the unlock screen if no authorized token
|
||||
* was found.
|
||||
/* A wrapper for <Route> that redirects to the unlock screen if no authorized
|
||||
* 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
|
||||
|
@ -66,7 +84,10 @@ function UserRoute({ children, ...rest }) {
|
|||
{...rest}
|
||||
render={({ match, location }) => {
|
||||
let state = { from: location, shareId: match.params.shareId };
|
||||
let authorized = jwt.hasSubject("admin") || (jwt.hasSubject("user") && jwt.hasClaim("id", state['shareId']));
|
||||
|
||||
let authorized =
|
||||
jwt.hasSubject("admin")
|
||||
|| ( jwt.hasSubject("user") && jwt.hasClaim("id", 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) {
|
||||
ev.preventDefault();
|
||||
|
||||
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 FlingList from './FlingList';
|
||||
import FlingContent from './FlingContent';
|
||||
|
||||
import {useParams} from 'react-router-dom';
|
||||
|
||||
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 (
|
||||
<div>
|
||||
|
@ -15,8 +28,12 @@ export default function FlingAdmin() {
|
|||
|
||||
<div className="container">
|
||||
<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"><FlingContent activeFling={fling} /></div>
|
||||
<div className="column col-sm-12 col-lg-3 col-2">
|
||||
<FlingList />
|
||||
</div>
|
||||
<div className="column col-sm-12">
|
||||
<FlingContent />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,33 +1,18 @@
|
|||
import log from 'loglevel';
|
||||
import React, {useState, useEffect} from 'react';
|
||||
|
||||
import {FlingClient} from '../../util/fc';
|
||||
import React from 'react';
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import FlingTile from './FlingTile';
|
||||
|
||||
export default function FlingList(props) {
|
||||
const [flings, setFlings] = useState([]);
|
||||
useEffect(() => {
|
||||
let flingClient = new FlingClient();
|
||||
flingClient.getFlings()
|
||||
.then(flings => {
|
||||
let newFlings = [];
|
||||
for (let fling of flings) {
|
||||
let flingTile = <FlingTile fling={fling} key={fling.id} />;
|
||||
newFlings.push(flingTile);
|
||||
}
|
||||
setFlings(newFlings);
|
||||
}).catch(log.error);
|
||||
}, []);
|
||||
export default function FlingList() {
|
||||
const flings = useSelector((store) => store.flings.flings);
|
||||
|
||||
return (
|
||||
<div className="panel">
|
||||
{log.info(`Got active fling: ${props.activeFling}`)}
|
||||
<div className="panel-header p-2">
|
||||
<h5>My Flings</h5>
|
||||
</div>
|
||||
<div className="panel-body p-0">
|
||||
{flings}
|
||||
{flings.map(fling => <FlingTile fling={fling} key={fling.id} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -16,21 +16,27 @@ function TileAction(props) {
|
|||
<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>
|
||||
<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" checked={props.fling.shared} onChange={toggleShared} />
|
||||
<input type="checkbox"
|
||||
checked={props.fling.shared} onChange={toggleShared} />
|
||||
<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={deleteFling}>
|
||||
<button className="btn btn-link text-warning pl-0"
|
||||
onClick={deleteFling}>
|
||||
<i className="icon icon-delete mr-1" /> Remove
|
||||
</button>
|
||||
</li>
|
||||
|
@ -74,7 +80,9 @@ export default function FlingTile(props) {
|
|||
<div className="tile-content">
|
||||
<NavLink to={`/admin/${props.fling.id}`}>
|
||||
<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">
|
||||
14MB · Public · 1 Jan, 2017
|
||||
</small>
|
||||
</NavLink>
|
||||
</div>
|
||||
<TileAction fling={props.fling} refreshFlingListFn={props.refreshFlingListFn} />
|
||||
|
|
|
@ -6,8 +6,9 @@ import ReactDOM from 'react-dom';
|
|||
import log from 'loglevel';
|
||||
|
||||
import { Provider } from 'react-redux';
|
||||
import { createStore } from 'redux';
|
||||
import reducer from './redux/reducer';
|
||||
import { createStore, applyMiddleware, compose } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import rootReducer from './redux/reducers';
|
||||
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
|
@ -24,9 +25,11 @@ if(process.env.REACT_APP_LOGLEVEL) {
|
|||
}
|
||||
|
||||
/* Store setup */
|
||||
let store = createStore(reducer,
|
||||
let store = createStore(rootReducer,
|
||||
compose(
|
||||
applyMiddleware(thunk),
|
||||
(window.__REDUX_DEVTOOLS_EXTENSION__
|
||||
&& window.__REDUX_DEVTOOLS_EXTENSION__()));
|
||||
&& window.__REDUX_DEVTOOLS_EXTENSION__())));
|
||||
|
||||
/* Fling App Setup */
|
||||
ReactDOM.render(
|
||||
|
|
3
web/fling/src/redux/actionTypes.js
Normal file
3
web/fling/src/redux/actionTypes.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const SET_FLINGS = "SET_FLINGS";
|
||||
export const ADD_FLING = "ADD_FLING";
|
||||
export const SET_ACTIVE_FLING = "SET_ACTIVE_FLING";
|
|
@ -0,0 +1,67 @@
|
|||
import log from 'loglevel';
|
||||
|
||||
import { SET_FLINGS, SET_ACTIVE_FLING, ADD_FLING } from "./actionTypes";
|
||||
import { FlingClient } from "../util/fc";
|
||||
|
||||
function setFlingsAction(flings) {
|
||||
return {
|
||||
type: SET_FLINGS,
|
||||
payload: flings
|
||||
}
|
||||
}
|
||||
|
||||
function addFlingAction(fling) {
|
||||
return {
|
||||
type: ADD_FLING,
|
||||
payload: fling
|
||||
}
|
||||
}
|
||||
|
||||
function setActiveFlingAction(fling) {
|
||||
return {
|
||||
type: SET_ACTIVE_FLING,
|
||||
payload: fling
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function setActiveFling(id) {
|
||||
return (dispatch, getState) => {
|
||||
if (!id) {
|
||||
log.debug(`Not setting active Fling. No id given.`);
|
||||
return;
|
||||
}
|
||||
const { flings: { flings } } = getState();
|
||||
let foundFling = flings.find(f => f.id === id);
|
||||
|
||||
if (foundFling) {
|
||||
log.info(`Found active fling ${id} in local storage`);
|
||||
dispatch(setActiveFlingAction(foundFling));
|
||||
} else {
|
||||
log.info(`Active fling ${id} not found in local storage. ` +
|
||||
`Trying to retrieve from remote.`);
|
||||
|
||||
let flingClient = new FlingClient();
|
||||
flingClient.getFling(id)
|
||||
.then(fling => {
|
||||
dispatch(addFlingAction(fling));
|
||||
dispatch(setActiveFlingAction(fling))
|
||||
})
|
||||
.catch(error => {
|
||||
log.warn(`Could not find active fling. ` +
|
||||
`Resetting active fling`);
|
||||
dispatch(setActiveFlingAction(undefined));
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function retrieveFlings() {
|
||||
return (dispatch) => {
|
||||
let flingClient = new FlingClient();
|
||||
flingClient.getFlings()
|
||||
.then(flings => dispatch(setFlingsAction(flings)));
|
||||
}
|
||||
}
|
||||
|
||||
export { retrieveFlings, setActiveFling };
|
|
@ -1,3 +0,0 @@
|
|||
export default function (state = {}, action) {
|
||||
return;
|
||||
};
|
29
web/fling/src/redux/reducers/flings.js
Normal file
29
web/fling/src/redux/reducers/flings.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import produce from "immer";
|
||||
|
||||
import { SET_FLINGS, SET_ACTIVE_FLING, ADD_FLING } from "../actionTypes";
|
||||
|
||||
const initialState = {
|
||||
// type [fc.Fling]
|
||||
flings: [],
|
||||
// fc.Fling.id of the currently active fling
|
||||
// or null of no fling is active
|
||||
activeFling: null
|
||||
}
|
||||
|
||||
export default produce((draft, action) => {
|
||||
switch (action.type) {
|
||||
case SET_FLINGS:
|
||||
draft.flings = action.payload;
|
||||
break;
|
||||
case ADD_FLING:
|
||||
draft.flings.push(action.payload);
|
||||
break;
|
||||
case SET_ACTIVE_FLING:
|
||||
draft.activeFling = action.payload;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return draft;
|
||||
|
||||
}, initialState);
|
4
web/fling/src/redux/reducers/index.js
Normal file
4
web/fling/src/redux/reducers/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { combineReducers } from "redux";
|
||||
import flings from "./flings";
|
||||
|
||||
export default combineReducers({ flings });
|
3
web/fling/src/redux/selectorTypes.js
Normal file
3
web/fling/src/redux/selectorTypes.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const FLING_FILTERS = {
|
||||
ALL: "all"
|
||||
}
|
0
web/fling/src/redux/selectors.js
Normal file
0
web/fling/src/redux/selectors.js
Normal file
10
web/fling/src/redux/selectors/flingSelectors.js
Normal file
10
web/fling/src/redux/selectors/flingSelectors.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { FLING_FILTERS } from "../selectorTypes";
|
||||
|
||||
export const flingSelector = (store, flingFilter) => {
|
||||
switch(flingFilter) {
|
||||
case FLING_FILTERS.ALL:
|
||||
return store.flings.flings;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
export default {
|
||||
flings: [],
|
||||
currentFling: undefined,
|
||||
};
|
|
@ -1,5 +1,12 @@
|
|||
/*
|
||||
* Shim for the fling API which sets a bearer token for every request
|
||||
*/
|
||||
import * as fc from '@fling/flingclient';
|
||||
|
||||
/*
|
||||
* Construct a client configuration with either the given token, or, if token is
|
||||
* undefined or null, token retrieved from the session storage.
|
||||
*/
|
||||
let clientConfig = (token) => {
|
||||
let config = new fc.ApiClient();
|
||||
config.basePath = process.env.REACT_APP_API.replace(/\/+$/, '');
|
||||
|
|
Loading…
Reference in a new issue