diff --git a/frontend/js/actions/users.js b/frontend/js/actions/users.js
index 686a951..d8d6844 100644
--- a/frontend/js/actions/users.js
+++ b/frontend/js/actions/users.js
@@ -50,3 +50,20 @@ export function getUserInfos() {
configureAxios().get("/users/details")
)
}
+
+export function getUserTokens() {
+ return request(
+ "GET_USER_TOKENS",
+ configureAxios().get("/users/tokens")
+ )
+}
+
+export function deleteUserToken(token) {
+ return request(
+ "DELETE_USER_TOKEN",
+ configureAxios().delete(`/users/tokens/${token}`),
+ [
+ () => getUserTokens(),
+ ]
+ )
+}
diff --git a/frontend/js/components/navbar.js b/frontend/js/components/navbar.js
index 19ae1f9..008a82b 100644
--- a/frontend/js/components/navbar.js
+++ b/frontend/js/components/navbar.js
@@ -164,6 +164,11 @@ function UserDropdown(props) {
}
+ {props.isActivated &&
+
+
+
+ }
diff --git a/frontend/js/components/users/tokens.js b/frontend/js/components/users/tokens.js
new file mode 100644
index 0000000..adb620b
--- /dev/null
+++ b/frontend/js/components/users/tokens.js
@@ -0,0 +1,187 @@
+import React from "react"
+import { connect } from "react-redux"
+import { UAParser } from "ua-parser-js"
+import moment from "moment"
+
+import { bindActionCreators } from "redux"
+import { deleteUserToken } from "../../actions/users"
+
+const mapDispatchToProps = (dispatch) =>
+ bindActionCreators({ deleteUserToken }, dispatch)
+
+function mapStateToProps(state) {
+ return {
+ tokens: state.userStore.get("tokens"),
+ };
+}
+
+function UserTokens(props) {
+ return (
+
+ );
+}
+
+function TokenList(props) {
+ return (
+
+
Tokens
+
Tokens
+
+
+ {props.tokens.map(function(el, index) {
+ return (
+
+ );
+ })}
+
+
+ );
+}
+
+function Token(props) {
+ const ua = UAParser(props.data.get("user_agent"));
+ return (
+
+
+
+
+
+ - Description
+ - {props.data.get("description")}
+
+ - Last IP
+ - {props.data.get("ip")}
+
+ - Last used
+ - {moment(props.data.get("last_used")).fromNow()}
+
+ - Created
+ - {moment(props.data.get("created_at")).fromNow()}
+
+ - Device
+
+
+ - OS
+
+
+ - Browser
+
+
+
+
+
+
+ );
+}
+
+class Actions extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.handleClick = this.handleClick.bind(this);
+ }
+ handleClick() {
+ const token = this.props.data.get("token");
+ this.props.deleteToken(token);
+ }
+ render() {
+ return (
+
+
+
+
+ );
+ }
+}
+
+function Logo(props) {
+ var className;
+ if (props.ua === "canape-cli") {
+ className = "terminal";
+ } else if (props.device.type == "mobile" ){
+ className = "mobile";
+ } else {
+ switch (props.browser.name) {
+ case "Chrome":
+ case "chrome":
+ className = "chrome";
+ break;
+ case "Safari":
+ case "safari":
+ className = "safari";
+ break;
+ case "Firefox":
+ case "firefox":
+ className = "firefox";
+ break;
+ default:
+ className = "question";
+ break;
+ }
+ }
+
+ return (
+
+ );
+}
+
+function OS(props) {
+ var osName = "-";
+
+ if (props.name !== undefined) {
+ osName = props.name;
+
+ if (props.version !== undefined) {
+ osName += " " + props.version;
+ }
+ }
+
+ return (
+ {osName}
+ );
+}
+
+function Device(props) {
+ var deviceName = "-";
+
+ if (props.model !== undefined) {
+ deviceName = props.model;
+ }
+
+ return (
+ {deviceName}
+ );
+}
+
+function Browser(props) {
+ var browserName = "-";
+ if (props.name !== undefined) {
+ browserName = props.name;
+
+ if (props.version !== undefined) {
+ browserName += " - " + props.version;
+ }
+ }
+
+ return (
+ {browserName}
+ );
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(UserTokens);
diff --git a/frontend/js/reducers/users.js b/frontend/js/reducers/users.js
index 8e133e7..116f4d6 100644
--- a/frontend/js/reducers/users.js
+++ b/frontend/js/reducers/users.js
@@ -1,4 +1,4 @@
-import { Map } from "immutable"
+import { Map, List, fromJS } from "immutable"
import jwtDecode from "jwt-decode"
import Cookies from "universal-cookie"
@@ -11,6 +11,7 @@ const defaultState = Map({
isLogged: false,
polochonToken: "",
polochonUrl: "",
+ tokens: List(),
});
const handlers = {
@@ -27,6 +28,9 @@ const handlers = {
polochonToken: action.payload.response.data.token,
polochonUrl: action.payload.response.data.url,
})),
+ "GET_USER_TOKENS_FULFILLED": (state, action) => state.set(
+ "tokens", fromJS(action.payload.response.data)
+ ),
}
function logoutUser() {
diff --git a/frontend/js/routes.js b/frontend/js/routes.js
index 20a0b91..86596db 100644
--- a/frontend/js/routes.js
+++ b/frontend/js/routes.js
@@ -3,6 +3,7 @@ import ShowList from "./components/shows/list"
import ShowDetails from "./components/shows/details"
import UserLoginForm from "./components/users/login"
import UserEdit from "./components/users/edit"
+import UserTokens from "./components/users/tokens"
import UserActivation from "./components/users/activation"
import UserSignUp from "./components/users/signup"
import TorrentList from "./components/torrents/list"
@@ -10,7 +11,7 @@ import TorrentSearch from "./components/torrents/search"
import AdminPanel from "./components/admins/panel"
import { fetchTorrents, searchTorrents } from "./actions/torrents"
-import { userLogout, getUserInfos } from "./actions/users"
+import { userLogout, getUserInfos, getUserTokens } from "./actions/users"
import { fetchMovies, getMovieExploreOptions } from "./actions/movies"
import { fetchShows, fetchShowDetails, getShowExploreOptions } from "./actions/shows"
import { getUsers, getStats } from "./actions/admins"
@@ -118,6 +119,15 @@ export default function getRoutes(App) {
});
},
},
+ {
+ path: "/users/tokens",
+ component: UserTokens,
+ onEnter: function(nextState, replace, next) {
+ loginCheck(nextState, replace, next, function() {
+ store.dispatch(getUserTokens());
+ });
+ },
+ },
{
path: "/users/activation",
component: UserActivation,
diff --git a/frontend/less/app.less b/frontend/less/app.less
index a59f0ae..4b51f59 100644
--- a/frontend/less/app.less
+++ b/frontend/less/app.less
@@ -53,6 +53,7 @@ body {
max-height: 300px;
}
+.spaced-icons,
.episode-buttons {
div, span {
margin: 2px;
@@ -103,3 +104,11 @@ table.torrent-search-result {
table.table-align-middle > tbody > tr > td {
vertical-align: middle;
}
+
+div.user-token-icon > div {
+ padding-top: 1%;
+}
+
+span.user-token-action {
+ margin: 10px;
+}
diff --git a/package.json b/package.json
index b77a583..e00a280 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"immutable": "^3.8.1",
"jquery": "^2.2.4",
"jwt-decode": "^2.1.0",
+ "moment": "^2.20.1",
"react": "^16.2.0",
"react-bootstrap": "^0.32.1",
"react-bootstrap-sweetalert": "^4.2.3",
@@ -31,6 +32,7 @@
"redux": "^3.7.2",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.2.0",
+ "ua-parser-js": "^0.7.17",
"universal-cookie": "^2.1.2",
"webpack": "^3.11.0"
},
diff --git a/yarn.lock b/yarn.lock
index 08429df..fdefbf5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2975,6 +2975,10 @@ minimist@^1.2.0:
dependencies:
minimist "0.0.8"
+moment@^2.20.1:
+ version "2.20.1"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.20.1.tgz#d6eb1a46cbcc14a2b2f9434112c1ff8907f313fd"
+
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -4399,7 +4403,7 @@ typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
-ua-parser-js@^0.7.9:
+ua-parser-js@^0.7.17, ua-parser-js@^0.7.9:
version "0.7.17"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac"