From 9aebee2c4abaf7e6bab5fa3a97a94044bdab70ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Sun, 13 Aug 2017 08:00:28 +0200 Subject: [PATCH 1/8] Add an "activated" field in the user table --- sql/dev/101_data.up.sql | 4 ++-- sql/migration/0005__user_activation.down.sql | 1 + sql/migration/0005__user_activation.up.sql | 3 +++ src/internal/users/users.go | 1 + 4 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 sql/migration/0005__user_activation.down.sql create mode 100644 sql/migration/0005__user_activation.up.sql diff --git a/sql/dev/101_data.up.sql b/sql/dev/101_data.up.sql index 74355ba..ec5228f 100644 --- a/sql/dev/101_data.up.sql +++ b/sql/dev/101_data.up.sql @@ -1,2 +1,2 @@ -INSERT INTO users (name, hash, admin) VALUES ('test', '$2a$10$QHx07iyuxO1RcehgtjMgjOzv03Bx2eeSKvsxkoj9oR2NJ4cklh6ue', false); -INSERT INTO users (name, hash, admin) VALUES ('admin', '$2a$10$qAbyDZsHtcnhXhjhQZkD2uKlX72eMHsX8Hi2Cnl1vJUqHQiey2qa6', true); +INSERT INTO users (name, hash, admin, activated) VALUES ('test', '$2a$10$QHx07iyuxO1RcehgtjMgjOzv03Bx2eeSKvsxkoj9oR2NJ4cklh6ue', false, true); +INSERT INTO users (name, hash, admin, activated) VALUES ('admin', '$2a$10$qAbyDZsHtcnhXhjhQZkD2uKlX72eMHsX8Hi2Cnl1vJUqHQiey2qa6', true, true); diff --git a/sql/migration/0005__user_activation.down.sql b/sql/migration/0005__user_activation.down.sql new file mode 100644 index 0000000..5e861f8 --- /dev/null +++ b/sql/migration/0005__user_activation.down.sql @@ -0,0 +1 @@ +ALTER TABLE users DROP COLUMN activated; diff --git a/sql/migration/0005__user_activation.up.sql b/sql/migration/0005__user_activation.up.sql new file mode 100644 index 0000000..8b6c34b --- /dev/null +++ b/sql/migration/0005__user_activation.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE users ADD COLUMN activated boolean NOT NULL DEFAULT false; +UPDATE users SET activated = true; +CREATE INDEX ON users (activated); diff --git a/src/internal/users/users.go b/src/internal/users/users.go index 861bcc1..880b0c4 100644 --- a/src/internal/users/users.go +++ b/src/internal/users/users.go @@ -44,6 +44,7 @@ type User struct { Name string Hash string Admin bool + Activated bool RawConfig types.JSONText } From 3f2fd351956ad4e84f68b6043083da23faeae9b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Sun, 13 Aug 2017 09:00:18 +0200 Subject: [PATCH 2/8] Only show the activation page for unactivated users --- src/internal/auth/auth.go | 1 + src/internal/auth/middleware.go | 8 +++++- src/internal/users/handlers.go | 5 ++-- src/internal/users/users.go | 5 ++++ src/public/js/app.js | 2 ++ src/public/js/components/navbar.js | 29 +++++++++++++------- src/public/js/components/users/activation.js | 16 +++++++++++ src/public/js/reducers/users.js | 2 ++ src/public/js/routes.js | 28 ++++++++++++++++--- 9 files changed, 79 insertions(+), 17 deletions(-) create mode 100644 src/public/js/components/users/activation.js diff --git a/src/internal/auth/auth.go b/src/internal/auth/auth.go index b072cb1..0fefd82 100644 --- a/src/internal/auth/auth.go +++ b/src/internal/auth/auth.go @@ -30,6 +30,7 @@ type User interface { GetHash() string HasRole(string) bool IsAdmin() bool + IsActivated() bool } // Authorizer handle sesssion diff --git a/src/internal/auth/middleware.go b/src/internal/auth/middleware.go index 758cfb9..1ae088f 100644 --- a/src/internal/auth/middleware.go +++ b/src/internal/auth/middleware.go @@ -74,7 +74,13 @@ func (m *MiddlewareRole) ServeHTTP(w http.ResponseWriter, r *http.Request, next return } - m.log.Debug("user has the role, continuing") + if !user.IsActivated() { + // return unauthorized + http.Error(w, "User is not activated", http.StatusUnauthorized) + return + } + + m.log.Debug("user has the role and is activated, continuing") next(w, r) } diff --git a/src/internal/users/handlers.go b/src/internal/users/handlers.go index 6adf924..d07223c 100644 --- a/src/internal/users/handlers.go +++ b/src/internal/users/handlers.go @@ -81,8 +81,9 @@ func LoginPOSTHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error // Issued at "iat": time.Now().Unix(), // Private claims - "username": user.GetName(), - "isAdmin": user.IsAdmin(), + "username": user.GetName(), + "isAdmin": user.IsAdmin(), + "isActivated": user.IsActivated(), }) // Sign the token diff --git a/src/internal/users/users.go b/src/internal/users/users.go index 880b0c4..d33e52d 100644 --- a/src/internal/users/users.go +++ b/src/internal/users/users.go @@ -251,3 +251,8 @@ func (u *User) HasRole(role string) bool { func (u *User) IsAdmin() bool { return u.HasRole(AdminRole) } + +// IsActivated checks if a user is activated +func (u *User) IsActivated() bool { + return u.Activated +} diff --git a/src/public/js/app.js b/src/public/js/app.js index 0fafb56..2f53839 100644 --- a/src/public/js/app.js +++ b/src/public/js/app.js @@ -47,6 +47,7 @@ function mapStateToProps(state) { return { username: state.userStore.get("username"), isAdmin: state.userStore.get("isAdmin"), + isActivated: state.userStore.get("isActivated"), torrentCount: torrentCount, alerts: state.alerts, } @@ -62,6 +63,7 @@ function Main(props) { diff --git a/src/public/js/components/navbar.js b/src/public/js/components/navbar.js index 5c44c5d..5c38db6 100644 --- a/src/public/js/components/navbar.js +++ b/src/public/js/components/navbar.js @@ -41,6 +41,9 @@ export default class AppNavBar extends React.PureComponent { this.setState({ expanded: value }); } render() { + const loggedAndActivated = (this.state.userLoggedIn && this.props.isActivated); + const displayShowsSearch = (this.state.displayShowsSearch && loggedAndActivated); + const displayMoviesSearch = (this.state.displayMoviesSearch && loggedAndActivated); return (
@@ -51,20 +54,24 @@ export default class AppNavBar extends React.PureComponent { - {this.state.userLoggedIn && + {loggedAndActivated && } - {this.state.userLoggedIn && + {loggedAndActivated && } - {this.state.userLoggedIn && + {loggedAndActivated && } - {this.state.userLoggedIn && + {loggedAndActivated && } - - {(this.state.displayMoviesSearch && this.state.userLoggedIn) && + + {displayMoviesSearch && } - {(this.state.displayShowsSearch && this.state.userLoggedIn) && + {displayShowsSearch && Admin Panel } - - Edit - + {props.isActivated && + + Edit + + } Logout diff --git a/src/public/js/components/users/activation.js b/src/public/js/components/users/activation.js new file mode 100644 index 0000000..a6f5288 --- /dev/null +++ b/src/public/js/components/users/activation.js @@ -0,0 +1,16 @@ +import React from "react" + +function UserActivation(props) { + return ( +
+
+
+

Waiting for activation

+
+

Hang tight! Your user will soon be activated by the administrators of this site.

+
+
+
+ ); +} +export default UserActivation; diff --git a/src/public/js/reducers/users.js b/src/public/js/reducers/users.js index 572f90c..8e133e7 100644 --- a/src/public/js/reducers/users.js +++ b/src/public/js/reducers/users.js @@ -7,6 +7,7 @@ const defaultState = Map({ loading: false, username: "", isAdmin: false, + isActivated: false, isLogged: false, polochonToken: "", polochonUrl: "", @@ -46,6 +47,7 @@ function updateFromToken(state, token) { userLoading: false, isLogged: true, isAdmin: decodedToken.isAdmin, + isActivated: decodedToken.isActivated, username: decodedToken.username, })) } diff --git a/src/public/js/routes.js b/src/public/js/routes.js index 4bde093..163ce69 100644 --- a/src/public/js/routes.js +++ b/src/public/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 UserActivation from "./components/users/activation" import UserSignUp from "./components/users/signup" import TorrentList from "./components/torrents/list" import AdminView from "./components/admins/users" @@ -42,15 +43,20 @@ function isLoggedIn() { return false } +function isActivated() { + const state = store.getState(); + return state.userStore.get("isActivated"); +} + var pollingTorrentsId; const loginCheck = function(nextState, replace, next, f = null) { const loggedIn = isLoggedIn(); if (!loggedIn) { - replace("/users/login"); + replace("/users/login"); + } else if (!isActivated()) { + replace("/users/activation"); } else { - if (f) { - f(); - } + if (f) { f(); } // Poll torrents once logged if (!pollingTorrentsId) { @@ -104,6 +110,20 @@ export default function getRoutes(App) { }); }, }, + { + path: "/users/activation", + component: UserActivation, + onEnter: function(nextState, replace, next) { + if (!isLoggedIn()) { + replace('/users/login'); + } + if (isActivated()) { + // User is already activated, redirect him to the default route + replace(defaultRoute); + } + next(); + }, + }, { path: "/users/logout", onEnter: function(nextState, replace, next) { From ebaf17e6e45ffa0fddf832b68d5a171db757b01e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Sun, 13 Aug 2017 10:10:24 +0200 Subject: [PATCH 3/8] Auto log user after register --- src/public/js/actions/users.js | 4 +++- src/public/js/components/users/signup.js | 15 ++++++++++++++- src/public/js/requests.js | 3 +++ src/public/js/routes.js | 9 ++++++++- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/public/js/actions/users.js b/src/public/js/actions/users.js index 61e9552..a06de7c 100644 --- a/src/public/js/actions/users.js +++ b/src/public/js/actions/users.js @@ -34,7 +34,9 @@ export function updateUser(config) { export function userSignUp(config) { return request( "USER_SIGNUP", - configureAxios().post("/users/signup", config) + configureAxios().post("/users/signup", config), [ + () => loginUser(config.username, config.password), + ], ) } diff --git a/src/public/js/components/users/signup.js b/src/public/js/components/users/signup.js index 80ec06b..d76d960 100644 --- a/src/public/js/components/users/signup.js +++ b/src/public/js/components/users/signup.js @@ -4,6 +4,12 @@ import { bindActionCreators } from "redux" import { userSignUp } from "../../actions/users" +function mapStateToProps(state) { + return { + isLogged: state.userStore.get("isLogged"), + }; +} + const mapDispatchToProps = (dispatch) => bindActionCreators({ userSignUp }, dispatch) @@ -20,6 +26,13 @@ class UserSignUp extends React.PureComponent { "password_confirm": this.refs.passwordConfirm.value, }); } + componentWillReceiveProps(nextProps) { + if (!nextProps.isLogged) { + return + } + // Redirect home + nextProps.router.push("/"); + } render() { return (
@@ -53,4 +66,4 @@ class UserSignUp extends React.PureComponent { ); } } -export default connect(null, mapDispatchToProps)(UserSignUp); +export default connect(mapStateToProps, mapDispatchToProps)(UserSignUp); diff --git a/src/public/js/requests.js b/src/public/js/requests.js index f264068..c47221f 100644 --- a/src/public/js/requests.js +++ b/src/public/js/requests.js @@ -50,6 +50,9 @@ export function request(eventPrefix, promise, callbackEvents = null, mainPayload }) if (callbackEvents) { for (let event of callbackEvents) { + if (typeof event === 'function') { + event = event(); + } dispatch(event); } } diff --git a/src/public/js/routes.js b/src/public/js/routes.js index 163ce69..e92298e 100644 --- a/src/public/js/routes.js +++ b/src/public/js/routes.js @@ -88,7 +88,14 @@ export default function getRoutes(App) { childRoutes: [ { path: "/users/signup", - component: UserSignUp + component: UserSignUp, + onEnter: function(nextState, replace, next) { + if (isLoggedIn()) { + // User is already logged in, redirect him to the default route + replace(defaultRoute); + } + next(); + }, }, { path: "/users/login", From a336c3a4dc04c6c909a0b071cdf313dd7591833e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Sun, 13 Aug 2017 10:14:48 +0200 Subject: [PATCH 4/8] Trim username spaces while logging in --- src/public/js/actions/users.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/public/js/actions/users.js b/src/public/js/actions/users.js index a06de7c..686a951 100644 --- a/src/public/js/actions/users.js +++ b/src/public/js/actions/users.js @@ -14,7 +14,7 @@ export function loginUser(username, password) { configureAxios().post( "/users/login", { - username: username, + username: username.trim(), password: password, }, ), @@ -32,6 +32,10 @@ export function updateUser(config) { } export function userSignUp(config) { + if (config.username) { + config.username = config.username.trim(); + } + return request( "USER_SIGNUP", configureAxios().post("/users/signup", config), [ From 667768fa10c8a78e3ec51fe429f1e026d5cd6ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Sun, 13 Aug 2017 10:19:10 +0200 Subject: [PATCH 5/8] Split admin panel in components --- src/public/js/components/admins/panel.js | 22 ++++++++++++++++++++++ src/public/js/components/admins/users.js | 21 +-------------------- src/public/js/routes.js | 4 ++-- 3 files changed, 25 insertions(+), 22 deletions(-) create mode 100644 src/public/js/components/admins/panel.js diff --git a/src/public/js/components/admins/panel.js b/src/public/js/components/admins/panel.js new file mode 100644 index 0000000..6e1dc3d --- /dev/null +++ b/src/public/js/components/admins/panel.js @@ -0,0 +1,22 @@ +import React from "react" +import { connect } from "react-redux" +import { bindActionCreators } from "redux" +import { getUsers } from "../../actions/admins" + +import UserList from "./users" + +function mapStateToProps(state) { + return { + users : state.adminStore.get("users"), + }; +} +const mapDispatchToProps = (dipatch) => + bindActionCreators({ getUsers }, dipatch) + +function AdminPanel(props) { + return ( + + ); +} + +export default connect(mapStateToProps, mapDispatchToProps)(AdminPanel); diff --git a/src/public/js/components/admins/users.js b/src/public/js/components/admins/users.js index d17c2c6..47a482c 100644 --- a/src/public/js/components/admins/users.js +++ b/src/public/js/components/admins/users.js @@ -1,23 +1,6 @@ import React from "react" -import { connect } from "react-redux" -import { bindActionCreators } from "redux" -import { getUsers } from "../../actions/admins" -function mapStateToProps(state) { - return { - users : state.adminStore.get("users"), - }; -} -const mapDispatchToProps = (dipatch) => - bindActionCreators({ getUsers }, dipatch) - -function AdminView(props) { - return ( - - ); -} - -function UserList(props) { +export default function UserList(props) { return (

Users

@@ -60,5 +43,3 @@ function User(props) { ); } - -export default connect(mapStateToProps, mapDispatchToProps)(AdminView); diff --git a/src/public/js/routes.js b/src/public/js/routes.js index e92298e..1bcea9b 100644 --- a/src/public/js/routes.js +++ b/src/public/js/routes.js @@ -6,7 +6,7 @@ import UserEdit from "./components/users/edit" import UserActivation from "./components/users/activation" import UserSignUp from "./components/users/signup" import TorrentList from "./components/torrents/list" -import AdminView from "./components/admins/users" +import AdminPanel from "./components/admins/panel" import { fetchTorrents } from "./actions/torrents" import { userLogout, getUserInfos } from "./actions/users" @@ -250,7 +250,7 @@ export default function getRoutes(App) { }, { path: "/admin", - component: AdminView, + component: AdminPanel, onEnter: function(nextState, replace, next) { adminCheck(nextState, replace, next, function() { store.dispatch(getUsers()); From dec954715200a539d0801e470fc67518e2cc9c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Sun, 13 Aug 2017 10:44:28 +0200 Subject: [PATCH 6/8] Display the activation state of the accounts --- package.json | 1 + src/internal/admins/users.go | 51 ++++++++ src/internal/users/users.go | 27 +++- src/public/js/actions/admins.js | 10 ++ src/public/js/components/admins/panel.js | 9 +- src/public/js/components/admins/users.js | 151 ++++++++++++++++++++++- src/public/less/app.less | 6 + src/routes.go | 1 + yarn.lock | 68 ++++++++++ 9 files changed, 311 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index ab243ef..5754776 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "react": "^15.3.2", "react-bootstrap": "^0.30.6", "react-bootstrap-sweetalert": "^3.0.0", + "react-bootstrap-toggle": "^2.0.8", "react-dom": "^15.3.2", "react-infinite-scroller": "^1.0.4", "react-loading": "^0.0.9", diff --git a/src/internal/admins/users.go b/src/internal/admins/users.go index 3a7ade1..04947bd 100644 --- a/src/internal/admins/users.go +++ b/src/internal/admins/users.go @@ -1,8 +1,11 @@ package admin import ( + "encoding/json" + "fmt" "net/http" + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" @@ -24,3 +27,51 @@ func GetUsersHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error return env.RenderJSON(w, users) } + +// UpdateUserHandler updates the user data +func UpdateUserHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { + log := env.Log.WithFields(logrus.Fields{ + "function": "admin.PostActivateUserHandler", + }) + + var data struct { + ID string `json:"userId"` + Admin bool `json:"admin"` + Activated bool `json:"activated"` + PolochonURL string `json:"polochonUrl"` + PolochonToken string `json:"polochonToken"` + } + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { + return err + } + + if data.ID == "" { + return env.RenderError(w, fmt.Errorf("Empty user id")) + } + + user, err := users.GetByID(env.Database, data.ID) + if err != nil { + return err + } + + // Update the polochon config + polochonConfig := &config.UserPolochon{ + URL: data.PolochonURL, + Token: data.PolochonToken, + } + if err := user.SetConfig("polochon", polochonConfig); err != nil { + return err + } + + user.Admin = data.Admin + user.Activated = data.Activated + + log.Debugf("updating user") + + // Save the user with the new configurations + if err := user.Update(env.Database); err != nil { + return err + } + + return env.RenderOK(w, "user updated") +} diff --git a/src/internal/users/users.go b/src/internal/users/users.go index d33e52d..90633de 100644 --- a/src/internal/users/users.go +++ b/src/internal/users/users.go @@ -1,6 +1,7 @@ package users import ( + "database/sql" "encoding/json" "errors" "fmt" @@ -15,17 +16,18 @@ import ( ) const ( - addUserQuery = `INSERT INTO users (name, hash, admin, rawconfig) VALUES ($1, $2, $3, $4) RETURNING id;` - getUserQuery = `SELECT * FROM users WHERE name=$1;` - updateUserQuery = `UPDATE users SET name=:name, hash=:hash, admin=:admin, rawconfig=:rawconfig WHERE id=:id RETURNING *;` - deleteUseQuery = `DELETE FROM users WHERE id=:id;` + addUserQuery = `INSERT INTO users (name, hash, admin, rawconfig) VALUES ($1, $2, $3, $4) RETURNING id;` + getUserQuery = `SELECT * FROM users WHERE name=$1;` + getUserByIDQuery = `SELECT * FROM users WHERE id=$1;` + updateUserQuery = `UPDATE users SET name=:name, hash=:hash, admin=:admin, activated=:activated, rawconfig=:rawconfig WHERE id=:id RETURNING *;` + deleteUseQuery = `DELETE FROM users WHERE id=:id;` addTokenQuery = `INSERT INTO tokens (value, user_id) VALUES ($1, $2) RETURNING id;` getTokensQuery = `SELECT id, value FROM tokens WHERE user_id=$1;` checkTokenQuery = `SELECT count(*) FROM tokens WHERE user_id=$1 AND value=$2;` deleteTokenQuery = `DELETE FROM tokens WHERE user_id=$1 AND value=$2;` - getAllUsersQuery = `SELECT * FROM users;` + getAllUsersQuery = `SELECT * FROM users order by created_at;` ) const ( @@ -128,7 +130,20 @@ func Get(q sqlx.Queryer, name string) (*User, error) { u := &User{} err := q.QueryRowx(getUserQuery, name).StructScan(u) if err != nil { - if err.Error() == "sql: no rows in result set" { + if err == sql.ErrNoRows { + return nil, ErrUnknownUser + } + return nil, err + } + return u, nil +} + +// GetByID returns user using its id +func GetByID(q sqlx.Queryer, id string) (*User, error) { + u := &User{} + err := q.QueryRowx(getUserByIDQuery, id).StructScan(u) + if err != nil { + if err == sql.ErrNoRows { return nil, ErrUnknownUser } return nil, err diff --git a/src/public/js/actions/admins.js b/src/public/js/actions/admins.js index 22759e1..15d2a65 100644 --- a/src/public/js/actions/admins.js +++ b/src/public/js/actions/admins.js @@ -6,3 +6,13 @@ export function getUsers() { configureAxios().get("/admins/users") ) } + +export function updateUser(data) { + return request( + "ADMIN_UPDATE_USER", + configureAxios().post("/admins/users", data), + [ + () => getUsers(), + ] + ) +} diff --git a/src/public/js/components/admins/panel.js b/src/public/js/components/admins/panel.js index 6e1dc3d..cd1847d 100644 --- a/src/public/js/components/admins/panel.js +++ b/src/public/js/components/admins/panel.js @@ -1,7 +1,7 @@ import React from "react" import { connect } from "react-redux" import { bindActionCreators } from "redux" -import { getUsers } from "../../actions/admins" +import { updateUser } from "../../actions/admins" import UserList from "./users" @@ -11,11 +11,14 @@ function mapStateToProps(state) { }; } const mapDispatchToProps = (dipatch) => - bindActionCreators({ getUsers }, dipatch) + bindActionCreators({ updateUser }, dipatch) function AdminPanel(props) { return ( - + ); } diff --git a/src/public/js/components/admins/users.js b/src/public/js/components/admins/users.js index 47a482c..ace4667 100644 --- a/src/public/js/components/admins/users.js +++ b/src/public/js/components/admins/users.js @@ -1,5 +1,8 @@ import React from "react" +import { Button, Modal } from "react-bootstrap" +import Toggle from "react-bootstrap-toggle"; + export default function UserList(props) { return (
@@ -10,15 +13,21 @@ export default function UserList(props) { # Name + Activated Admin Polochon URL Polochon token + Actions {props.users.map(function(el, index) { return ( - + ); })} @@ -31,15 +40,149 @@ function User(props) { const polochonConfig = props.data.get("RawConfig").get("polochon"); const polochonURL = polochonConfig ? polochonConfig.get("url") : "-"; const polochonToken = polochonConfig ? polochonConfig.get("token") : "-"; - const admin = props.data.get("Admin") ? "yes" : "no"; return ( {props.data.get("id")} {props.data.get("Name")} - {admin} + + {polochonURL} {polochonToken} - + + + ); } + +function UserAdminStatus(props) { + const admin = props.data.get("Admin"); + const className = admin ? "fa fa-check" : "fa fa-times"; + return (); +} + +function UserActivationStatus(props) { + const activated = props.data.get("Activated"); + const className = activated ? "fa fa-check" : "fa fa-times text-danger"; + return (); +} + +class UserEdit extends React.PureComponent { + constructor(props) { + super(props); + + // User props + const polochonConfig = props.data.get("RawConfig").get("polochon"); + const polochonUrl = polochonConfig ? polochonConfig.get("url") : ""; + const polochonToken = polochonConfig ? polochonConfig.get("token") : ""; + + // Functions + this.showModal = this.showModal.bind(this); + this.hideModal = this.hideModal.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.handleUrlInput = this.handleUrlInput.bind(this); + this.handleTokenInput = this.handleTokenInput.bind(this); + this.handleActivatedToggle = this.handleActivatedToggle.bind(this); + this.handleAdminToggle = this.handleAdminToggle.bind(this); + + this.state = { + showModal: false, + polochonUrl: polochonUrl, + polochonToken: polochonToken, + activated: props.data.get("Activated"), + admin: props.data.get("Admin"), + }; + } + showModal() { + this.setState({ showModal: true }); + } + hideModal() { + this.setState({ showModal: false }); + } + handleSubmit(e) { + if (e) { e.preventDefault(); } + this.props.updateUser({ + userId: this.props.data.get("id"), + admin: this.state.admin, + activated: this.state.activated, + polochonUrl: this.state.polochonUrl, + polochonToken: this.state.polochonToken, + }); + this.setState({ showModal: false }); + } + handleTokenInput() { + this.setState({ polochonToken: this.refs.polochonToken.value }); + } + handleUrlInput() { + this.setState({ polochonUrl: this.refs.polochonUrl.value }); + } + handleActivatedToggle() { + this.setState({ activated: !this.state.activated }); + } + handleAdminToggle() { + this.setState({ admin: !this.state.admin }); + } + render() { + return ( + + + + + +   Edit user - {this.props.data.get("Name")} + + + +
this.handleSubmit(ev)}> +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + + +
+
+ ); + } +} diff --git a/src/public/less/app.less b/src/public/less/app.less index 1f866ce..c6fbf3f 100644 --- a/src/public/less/app.less +++ b/src/public/less/app.less @@ -2,6 +2,7 @@ @import "~bootswatch/superhero/variables.less"; @import "~bootswatch/superhero/bootswatch.less"; @import "~font-awesome/less/font-awesome.less"; +@import "~react-bootstrap-toggle/src/bootstrap2-toggle.css"; body { padding-top: @navbar-height + 10px; @@ -75,3 +76,8 @@ div.sweet-alert > h2 { .player-modal { width: 90%; } + +.admin-edit-user-modal { + margin-left: 3px; + margin-right: 3px; +} diff --git a/src/routes.go b/src/routes.go index 6e2af31..f2d07df 100644 --- a/src/routes.go +++ b/src/routes.go @@ -60,4 +60,5 @@ func setupRoutes(env *web.Env) { // Admin routes env.Handle("/admins/users", admin.GetUsersHandler).WithRole(users.AdminRole).Methods("GET") + env.Handle("/admins/users", admin.UpdateUserHandler).WithRole(users.AdminRole).Methods("POST") } diff --git a/yarn.lock b/yarn.lock index d3d2d2e..5b264f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1125,6 +1125,14 @@ core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" +create-react-class@^15.6.0: + version "15.6.0" + resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.0.tgz#ab448497c26566e1e29413e883207d57cfe7bed4" + dependencies: + fbjs "^0.8.9" + loose-envify "^1.3.1" + object-assign "^4.1.1" + cryptiles@2.x.x: version "2.0.5" resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" @@ -1613,6 +1621,18 @@ fbjs@^0.8.4: promise "^7.1.1" ua-parser-js "^0.7.9" +fbjs@^0.8.9: + version "0.8.14" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.14.tgz#d1dbe2be254c35a91e09f31f9cd50a40b2a0ed1c" + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.9" + figures@^1.3.5: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" @@ -2703,6 +2723,12 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0: dependencies: js-tokens "^2.0.0" +loose-envify@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" + dependencies: + js-tokens "^3.0.0" + loud-rejection@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" @@ -2971,6 +2997,10 @@ object-assign@^4.0.1, object-assign@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0" +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + object.omit@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" @@ -3406,6 +3436,13 @@ promise@^7.1.1: dependencies: asap "~2.0.3" +prop-types@^15.5.10: + version "15.5.10" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154" + dependencies: + fbjs "^0.8.9" + loose-envify "^1.3.1" + prr@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" @@ -3477,10 +3514,27 @@ react-bootstrap-sweetalert: dependencies: object-assign "^4.1.0" +react-bootstrap-toggle@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/react-bootstrap-toggle/-/react-bootstrap-toggle-2.0.8.tgz#04e951527ffdd3b2a18de753b4992fa2a7d25792" + dependencies: + prop-types "^15.5.10" + react "^15.4.2" + react-dom "^15.4.2" + react-dom@^15.3.2: version "15.3.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.3.2.tgz#c46b0aa5380d7b838e7a59c4a7beff2ed315531f" +react-dom@^15.4.2: + version "15.6.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.1.tgz#2cb0ed4191038e53c209eb3a79a23e2a4cf99470" + dependencies: + fbjs "^0.8.9" + loose-envify "^1.1.0" + object-assign "^4.1.0" + prop-types "^15.5.10" + react-infinite-scroller: version "1.0.4" resolved "https://registry.yarnpkg.com/react-infinite-scroller/-/react-infinite-scroller-1.0.4.tgz#cb171113c4c8ee6aa44669392a55ad50938e2028" @@ -3539,6 +3593,16 @@ react@^15.3.2: loose-envify "^1.1.0" object-assign "^4.1.0" +react@^15.4.2: + version "15.6.1" + resolved "https://registry.yarnpkg.com/react/-/react-15.6.1.tgz#baa8434ec6780bde997cdc380b79cd33b96393df" + dependencies: + create-react-class "^15.6.0" + fbjs "^0.8.9" + loose-envify "^1.1.0" + object-assign "^4.1.0" + prop-types "^15.5.10" + read-pkg-up@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" @@ -3845,6 +3909,10 @@ set-immediate-shim@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + sha.js@2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.2.6.tgz#17ddeddc5f722fb66501658895461977867315ba" From eb02ff2b46636e75c4b4126d0b39b0b549d1f572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Mon, 14 Aug 2017 00:20:38 +0200 Subject: [PATCH 7/8] Add stats about the library --- src/internal/admins/stats.go | 57 ++++++++++++++++++++++++ src/public/js/actions/admins.js | 7 +++ src/public/js/components/admins/panel.js | 15 +++++-- src/public/js/components/admins/stats.js | 27 +++++++++++ src/public/js/reducers/admins.js | 2 + src/public/js/routes.js | 5 ++- src/routes.go | 1 + 7 files changed, 108 insertions(+), 6 deletions(-) create mode 100644 src/internal/admins/stats.go create mode 100644 src/public/js/components/admins/stats.js diff --git a/src/internal/admins/stats.go b/src/internal/admins/stats.go new file mode 100644 index 0000000..0897d2c --- /dev/null +++ b/src/internal/admins/stats.go @@ -0,0 +1,57 @@ +package admin + +import ( + "net/http" + + "github.com/jmoiron/sqlx" + "github.com/sirupsen/logrus" + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" +) + +const ( + moviesCountQuery = `SELECT COUNT(*) FROM movies;` + showsCountQuery = `SELECT COUNT(*) FROM shows;` + episodesCountQuery = `SELECT COUNT(*) FROM episodes;` +) + +// GetCount gets the count from a query +func GetCount(db *sqlx.DB, query string) (int, error) { + var count int + err := db.QueryRow(query).Scan(&count) + if err != nil { + return 0, err + } + return count, nil +} + +// GetStatsHandler returns the stats of the app +func GetStatsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { + log := env.Log.WithFields(logrus.Fields{ + "function": "admin.GetStatsHandler", + }) + + log.Debug("getting stats") + + stats := struct { + MoviesCount int `json:"movies_count"` + ShowsCount int `json:"shows_count"` + EpisodesCount int `json:"episodes_count"` + }{} + + for _, s := range []struct { + query string + ptr *int + }{ + {moviesCountQuery, &stats.MoviesCount}, + {showsCountQuery, &stats.ShowsCount}, + {episodesCountQuery, &stats.EpisodesCount}, + } { + var err error + *s.ptr, err = GetCount(env.Database, s.query) + if err != nil { + return err + } + } + + return env.RenderJSON(w, stats) +} diff --git a/src/public/js/actions/admins.js b/src/public/js/actions/admins.js index 15d2a65..050ca88 100644 --- a/src/public/js/actions/admins.js +++ b/src/public/js/actions/admins.js @@ -7,6 +7,13 @@ export function getUsers() { ) } +export function getStats() { + return request( + "ADMIN_GET_STATS", + configureAxios().get("/admins/stats") + ) +} + export function updateUser(data) { return request( "ADMIN_UPDATE_USER", diff --git a/src/public/js/components/admins/panel.js b/src/public/js/components/admins/panel.js index cd1847d..5f5e5f0 100644 --- a/src/public/js/components/admins/panel.js +++ b/src/public/js/components/admins/panel.js @@ -4,10 +4,12 @@ import { bindActionCreators } from "redux" import { updateUser } from "../../actions/admins" import UserList from "./users" +import Stats from "./stats" function mapStateToProps(state) { return { users : state.adminStore.get("users"), + stats : state.adminStore.get("stats"), }; } const mapDispatchToProps = (dipatch) => @@ -15,10 +17,15 @@ const mapDispatchToProps = (dipatch) => function AdminPanel(props) { return ( - +
+ + +
); } diff --git a/src/public/js/components/admins/stats.js b/src/public/js/components/admins/stats.js new file mode 100644 index 0000000..3993dbb --- /dev/null +++ b/src/public/js/components/admins/stats.js @@ -0,0 +1,27 @@ +import React from "react" + +export default function Stats(props) { + return ( +
+

Stats

+ + + +
+ ); +} + +function Stat(props) { + return ( +
+
+
+

{props.name}

+
+
+ {props.value} +
+
+
+ ); +} diff --git a/src/public/js/reducers/admins.js b/src/public/js/reducers/admins.js index caac3b6..a309c46 100644 --- a/src/public/js/reducers/admins.js +++ b/src/public/js/reducers/admins.js @@ -2,10 +2,12 @@ import { Map, List, fromJS } from "immutable" const defaultState = Map({ "users": List(), + "stats": Map({}), }); const handlers = { "ADMIN_LIST_USERS_FULFILLED": (state, action) => state.set("users", fromJS(action.payload.response.data)), + "ADMIN_GET_STATS_FULFILLED": (state, action) => state.set("stats", fromJS(action.payload.response.data)), } export default (state = defaultState, action) => diff --git a/src/public/js/routes.js b/src/public/js/routes.js index 1bcea9b..3f1fcd1 100644 --- a/src/public/js/routes.js +++ b/src/public/js/routes.js @@ -12,7 +12,7 @@ import { fetchTorrents } from "./actions/torrents" import { userLogout, getUserInfos } from "./actions/users" import { fetchMovies, getMovieExploreOptions } from "./actions/movies" import { fetchShows, fetchShowDetails, getShowExploreOptions } from "./actions/shows" -import { getUsers } from "./actions/admins" +import { getUsers, getStats } from "./actions/admins" import store from "./store" @@ -122,7 +122,7 @@ export default function getRoutes(App) { component: UserActivation, onEnter: function(nextState, replace, next) { if (!isLoggedIn()) { - replace('/users/login'); + replace("/users/login"); } if (isActivated()) { // User is already activated, redirect him to the default route @@ -254,6 +254,7 @@ export default function getRoutes(App) { onEnter: function(nextState, replace, next) { adminCheck(nextState, replace, next, function() { store.dispatch(getUsers()); + store.dispatch(getStats()); }); }, }, diff --git a/src/routes.go b/src/routes.go index f2d07df..0c56a58 100644 --- a/src/routes.go +++ b/src/routes.go @@ -61,4 +61,5 @@ func setupRoutes(env *web.Env) { // Admin routes env.Handle("/admins/users", admin.GetUsersHandler).WithRole(users.AdminRole).Methods("GET") env.Handle("/admins/users", admin.UpdateUserHandler).WithRole(users.AdminRole).Methods("POST") + env.Handle("/admins/stats", admin.GetStatsHandler).WithRole(users.AdminRole).Methods("GET") } From 24d3e0eaee0757f5b220ed371790dab6f20a71e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Mon, 14 Aug 2017 01:11:27 +0200 Subject: [PATCH 8/8] Add torrents stats --- src/internal/admins/stats.go | 24 +++++++++++---- src/public/js/components/admins/stats.js | 37 ++++++++++++++++++++---- 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/src/internal/admins/stats.go b/src/internal/admins/stats.go index 0897d2c..b33e735 100644 --- a/src/internal/admins/stats.go +++ b/src/internal/admins/stats.go @@ -9,9 +9,13 @@ import ( ) const ( - moviesCountQuery = `SELECT COUNT(*) FROM movies;` - showsCountQuery = `SELECT COUNT(*) FROM shows;` - episodesCountQuery = `SELECT COUNT(*) FROM episodes;` + moviesCountQuery = `SELECT COUNT(*) FROM movies;` + moviesTorrentsCountByIDQuery = `SELECT COUNT(*) FROM (SELECT DISTINCT(imdb_id) FROM movie_torrents) as TMP;` + moviesTorrentsCountQuery = `SELECT COUNT(*) FROM movie_torrents;` + showsCountQuery = `SELECT COUNT(*) FROM shows;` + episodesCountQuery = `SELECT COUNT(*) FROM episodes;` + episodesTorrentsCountByIDQuery = `SELECT COUNT(*) FROM (SELECT DISTINCT(imdb_id) FROM episode_torrents) as TMP;` + episodesTorrentsCountQuery = `SELECT COUNT(*) FROM episode_torrents;` ) // GetCount gets the count from a query @@ -33,9 +37,13 @@ func GetStatsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error log.Debug("getting stats") stats := struct { - MoviesCount int `json:"movies_count"` - ShowsCount int `json:"shows_count"` - EpisodesCount int `json:"episodes_count"` + MoviesCount int `json:"movies_count"` + MoviesTorrentsCount int `json:"movies_torrents_count"` + MoviesTorrentsCountByID int `json:"movies_torrents_count_by_id"` + ShowsCount int `json:"shows_count"` + EpisodesCount int `json:"episodes_count"` + EpisodesTorrentsCount int `json:"episodes_torrents_count"` + EpisodesTorrentsCountByID int `json:"episodes_torrents_count_by_id"` }{} for _, s := range []struct { @@ -43,8 +51,12 @@ func GetStatsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error ptr *int }{ {moviesCountQuery, &stats.MoviesCount}, + {moviesTorrentsCountQuery, &stats.MoviesTorrentsCount}, + {moviesTorrentsCountByIDQuery, &stats.MoviesTorrentsCountByID}, {showsCountQuery, &stats.ShowsCount}, {episodesCountQuery, &stats.EpisodesCount}, + {episodesTorrentsCountQuery, &stats.EpisodesTorrentsCount}, + {episodesTorrentsCountByIDQuery, &stats.EpisodesTorrentsCountByID}, } { var err error *s.ptr, err = GetCount(env.Database, s.query) diff --git a/src/public/js/components/admins/stats.js b/src/public/js/components/admins/stats.js index 3993dbb..4b0b145 100644 --- a/src/public/js/components/admins/stats.js +++ b/src/public/js/components/admins/stats.js @@ -4,9 +4,19 @@ export default function Stats(props) { return (

Stats

- - - + + +
); } @@ -16,12 +26,29 @@ function Stat(props) {
-

{props.name}

+

+ {props.name} + {props.count} +

- {props.value} +
); } + +function TorrentsStat(props) { + if (props.data.torrentCount === undefined) { + return (No torrents); + } + + const percentage = Math.floor((props.data.torrentCount * 100) / props.data.count); + return ( + + {percentage}% with torrents +   - {props.data.torrentCount} total + + ); +}