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) { Edit } + {props.isActivated && + + Tokens + + } Logout 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"