Merge branch 'admin' into 'master'

Admin panel

See merge request !87
This commit is contained in:
Lucas 2017-08-14 08:25:42 +00:00
commit 4568b8884e
26 changed files with 592 additions and 54 deletions

View File

@ -17,6 +17,7 @@
"react": "^15.3.2", "react": "^15.3.2",
"react-bootstrap": "^0.30.6", "react-bootstrap": "^0.30.6",
"react-bootstrap-sweetalert": "^3.0.0", "react-bootstrap-sweetalert": "^3.0.0",
"react-bootstrap-toggle": "^2.0.8",
"react-dom": "^15.3.2", "react-dom": "^15.3.2",
"react-infinite-scroller": "^1.0.4", "react-infinite-scroller": "^1.0.4",
"react-loading": "^0.0.9", "react-loading": "^0.0.9",

View File

@ -1,2 +1,2 @@
INSERT INTO users (name, hash, admin) VALUES ('test', '$2a$10$QHx07iyuxO1RcehgtjMgjOzv03Bx2eeSKvsxkoj9oR2NJ4cklh6ue', false); INSERT INTO users (name, hash, admin, activated) VALUES ('test', '$2a$10$QHx07iyuxO1RcehgtjMgjOzv03Bx2eeSKvsxkoj9oR2NJ4cklh6ue', false, true);
INSERT INTO users (name, hash, admin) VALUES ('admin', '$2a$10$qAbyDZsHtcnhXhjhQZkD2uKlX72eMHsX8Hi2Cnl1vJUqHQiey2qa6', true); INSERT INTO users (name, hash, admin, activated) VALUES ('admin', '$2a$10$qAbyDZsHtcnhXhjhQZkD2uKlX72eMHsX8Hi2Cnl1vJUqHQiey2qa6', true, true);

View File

@ -0,0 +1 @@
ALTER TABLE users DROP COLUMN activated;

View File

@ -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);

View File

@ -0,0 +1,69 @@
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;`
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
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"`
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 {
query string
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)
if err != nil {
return err
}
}
return env.RenderJSON(w, stats)
}

View File

@ -1,8 +1,11 @@
package admin package admin
import ( import (
"encoding/json"
"fmt"
"net/http" "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/users"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" "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) 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")
}

View File

@ -30,6 +30,7 @@ type User interface {
GetHash() string GetHash() string
HasRole(string) bool HasRole(string) bool
IsAdmin() bool IsAdmin() bool
IsActivated() bool
} }
// Authorizer handle sesssion // Authorizer handle sesssion

View File

@ -74,7 +74,13 @@ func (m *MiddlewareRole) ServeHTTP(w http.ResponseWriter, r *http.Request, next
return 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) next(w, r)
} }

View File

@ -81,8 +81,9 @@ func LoginPOSTHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error
// Issued at // Issued at
"iat": time.Now().Unix(), "iat": time.Now().Unix(),
// Private claims // Private claims
"username": user.GetName(), "username": user.GetName(),
"isAdmin": user.IsAdmin(), "isAdmin": user.IsAdmin(),
"isActivated": user.IsActivated(),
}) })
// Sign the token // Sign the token

View File

@ -1,6 +1,7 @@
package users package users
import ( import (
"database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -15,17 +16,18 @@ import (
) )
const ( const (
addUserQuery = `INSERT INTO users (name, hash, admin, rawconfig) VALUES ($1, $2, $3, $4) RETURNING id;` addUserQuery = `INSERT INTO users (name, hash, admin, rawconfig) VALUES ($1, $2, $3, $4) RETURNING id;`
getUserQuery = `SELECT * FROM users WHERE name=$1;` getUserQuery = `SELECT * FROM users WHERE name=$1;`
updateUserQuery = `UPDATE users SET name=:name, hash=:hash, admin=:admin, rawconfig=:rawconfig WHERE id=:id RETURNING *;` getUserByIDQuery = `SELECT * FROM users WHERE id=$1;`
deleteUseQuery = `DELETE FROM users WHERE id=:id;` 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;` addTokenQuery = `INSERT INTO tokens (value, user_id) VALUES ($1, $2) RETURNING id;`
getTokensQuery = `SELECT id, value FROM tokens WHERE user_id=$1;` getTokensQuery = `SELECT id, value FROM tokens WHERE user_id=$1;`
checkTokenQuery = `SELECT count(*) FROM tokens WHERE user_id=$1 AND value=$2;` checkTokenQuery = `SELECT count(*) FROM tokens WHERE user_id=$1 AND value=$2;`
deleteTokenQuery = `DELETE 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 ( const (
@ -44,6 +46,7 @@ type User struct {
Name string Name string
Hash string Hash string
Admin bool Admin bool
Activated bool
RawConfig types.JSONText RawConfig types.JSONText
} }
@ -127,7 +130,20 @@ func Get(q sqlx.Queryer, name string) (*User, error) {
u := &User{} u := &User{}
err := q.QueryRowx(getUserQuery, name).StructScan(u) err := q.QueryRowx(getUserQuery, name).StructScan(u)
if err != nil { 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, ErrUnknownUser
} }
return nil, err return nil, err
@ -250,3 +266,8 @@ func (u *User) HasRole(role string) bool {
func (u *User) IsAdmin() bool { func (u *User) IsAdmin() bool {
return u.HasRole(AdminRole) return u.HasRole(AdminRole)
} }
// IsActivated checks if a user is activated
func (u *User) IsActivated() bool {
return u.Activated
}

View File

@ -6,3 +6,20 @@ export function getUsers() {
configureAxios().get("/admins/users") configureAxios().get("/admins/users")
) )
} }
export function getStats() {
return request(
"ADMIN_GET_STATS",
configureAxios().get("/admins/stats")
)
}
export function updateUser(data) {
return request(
"ADMIN_UPDATE_USER",
configureAxios().post("/admins/users", data),
[
() => getUsers(),
]
)
}

View File

@ -14,7 +14,7 @@ export function loginUser(username, password) {
configureAxios().post( configureAxios().post(
"/users/login", "/users/login",
{ {
username: username, username: username.trim(),
password: password, password: password,
}, },
), ),
@ -32,9 +32,15 @@ export function updateUser(config) {
} }
export function userSignUp(config) { export function userSignUp(config) {
if (config.username) {
config.username = config.username.trim();
}
return request( return request(
"USER_SIGNUP", "USER_SIGNUP",
configureAxios().post("/users/signup", config) configureAxios().post("/users/signup", config), [
() => loginUser(config.username, config.password),
],
) )
} }

View File

@ -47,6 +47,7 @@ function mapStateToProps(state) {
return { return {
username: state.userStore.get("username"), username: state.userStore.get("username"),
isAdmin: state.userStore.get("isAdmin"), isAdmin: state.userStore.get("isAdmin"),
isActivated: state.userStore.get("isActivated"),
torrentCount: torrentCount, torrentCount: torrentCount,
alerts: state.alerts, alerts: state.alerts,
} }
@ -62,6 +63,7 @@ function Main(props) {
<NavBar <NavBar
username={props.username} username={props.username}
isAdmin={props.isAdmin} isAdmin={props.isAdmin}
isActivated={props.isActivated}
router={props.router} router={props.router}
torrentCount={props.torrentCount} torrentCount={props.torrentCount}
/> />

View File

@ -0,0 +1,32 @@
import React from "react"
import { connect } from "react-redux"
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) =>
bindActionCreators({ updateUser }, dipatch)
function AdminPanel(props) {
return (
<div>
<Stats
stats={props.stats}
/>
<UserList
users={props.users}
updateUser={props.updateUser}
/>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(AdminPanel);

View File

@ -0,0 +1,54 @@
import React from "react"
export default function Stats(props) {
return (
<div>
<h2 className="hidden-xs">Stats</h2>
<Stat
name="Movies"
count={props.stats.get("movies_count")}
torrentCount={props.stats.get("movies_torrents_count")}
torrentCountById={props.stats.get("movies_torrents_count_by_id")}
/>
<Stat name="Shows" count={props.stats.get("shows_count")} />
<Stat
name="Episodes"
count={props.stats.get("episodes_count")}
torrentCount={props.stats.get("episodes_torrents_count")}
torrentCountById={props.stats.get("episodes_torrents_count_by_id")}
/>
</div>
);
}
function Stat(props) {
return (
<div className="col-xs-4">
<div className="panel panel-default">
<div className="panel-heading">
<h3 className="panel-title">
{props.name}
<span className="label label-info pull-right">{props.count}</span>
</h3>
</div>
<div className="panel-body">
<TorrentsStat data={props} />
</div>
</div>
</div>
);
}
function TorrentsStat(props) {
if (props.data.torrentCount === undefined) {
return (<span>No torrents</span>);
}
const percentage = Math.floor((props.data.torrentCount * 100) / props.data.count);
return (
<span>
{percentage}% with torrents
<small>&nbsp; - {props.data.torrentCount} total</small>
</span>
);
}

View File

@ -1,23 +1,9 @@
import React from "react" import React from "react"
import { connect } from "react-redux"
import { bindActionCreators } from "redux"
import { getUsers } from "../../actions/admins"
function mapStateToProps(state) { import { Button, Modal } from "react-bootstrap"
return { import Toggle from "react-bootstrap-toggle";
users : state.adminStore.get("users"),
};
}
const mapDispatchToProps = (dipatch) =>
bindActionCreators({ getUsers }, dipatch)
function AdminView(props) { export default function UserList(props) {
return (
<UserList users={props.users} />
);
}
function UserList(props) {
return ( return (
<div> <div>
<h2 className="hidden-xs">Users</h2> <h2 className="hidden-xs">Users</h2>
@ -27,15 +13,21 @@ function UserList(props) {
<tr className="active"> <tr className="active">
<th>#</th> <th>#</th>
<th>Name</th> <th>Name</th>
<th>Activated</th>
<th>Admin</th> <th>Admin</th>
<th>Polochon URL</th> <th>Polochon URL</th>
<th>Polochon token</th> <th>Polochon token</th>
<th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{props.users.map(function(el, index) { {props.users.map(function(el, index) {
return ( return (
<User key={index} data={el} /> <User
key={index}
data={el}
updateUser={props.updateUser}
/>
); );
})} })}
</tbody> </tbody>
@ -48,17 +40,149 @@ function User(props) {
const polochonConfig = props.data.get("RawConfig").get("polochon"); const polochonConfig = props.data.get("RawConfig").get("polochon");
const polochonURL = polochonConfig ? polochonConfig.get("url") : "-"; const polochonURL = polochonConfig ? polochonConfig.get("url") : "-";
const polochonToken = polochonConfig ? polochonConfig.get("token") : "-"; const polochonToken = polochonConfig ? polochonConfig.get("token") : "-";
const admin = props.data.get("Admin") ? "yes" : "no";
return ( return (
<tr> <tr>
<td>{props.data.get("id")}</td> <td>{props.data.get("id")}</td>
<td>{props.data.get("Name")}</td> <td>{props.data.get("Name")}</td>
<td>{admin}</td> <td><UserActivationStatus data={props.data}/></td>
<td><UserAdminStatus data={props.data}/></td>
<td>{polochonURL}</td> <td>{polochonURL}</td>
<td>{polochonToken}</td> <td>{polochonToken}</td>
<td></td> <td>
<UserEdit
data={props.data}
updateUser={props.updateUser}
/>
</td>
</tr> </tr>
); );
} }
export default connect(mapStateToProps, mapDispatchToProps)(AdminView); function UserAdminStatus(props) {
const admin = props.data.get("Admin");
const className = admin ? "fa fa-check" : "fa fa-times";
return (<span className={className}></span>);
}
function UserActivationStatus(props) {
const activated = props.data.get("Activated");
const className = activated ? "fa fa-check" : "fa fa-times text-danger";
return (<span className={className}></span>);
}
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 (
<span className="fa fa-pencil clickable" onClick={this.showModal}>
<Modal show={this.state.showModal} onHide={this.hideModal}>
<Modal.Header closeButton>
<Modal.Title>
<i className="fa fa-pencil"></i>
&nbsp; Edit user - {this.props.data.get("Name")}
</Modal.Title>
</Modal.Header>
<Modal.Body bsClass="modal-body admin-edit-user-modal">
<form className="form-horizontal" onSubmit={(ev) => this.handleSubmit(ev)}>
<div className="form-group">
<label>Account status</label>
<Toggle
className="pull-right"
on="Activated"
off="Deactivated"
active={this.state.activated}
onClick={this.handleActivatedToggle}
/>
</div>
<div className="form-group">
<label>Admin status</label>
<Toggle
className="pull-right"
on="Admin"
off="User"
active={this.state.admin}
onClick={this.handleAdminToggle}
/>
</div>
<div className="form-group">
<label className="control-label">Polochon URL</label>
<input
className="form-control"
value={this.state.polochonUrl}
onChange={this.handleUrlInput}
ref="polochonUrl"
/>
</div>
<div className="form-group">
<label className="control-label">Polochon token</label>
<input
className="form-control"
value={this.state.polochonToken}
onChange={this.handleTokenInput}
ref="polochonToken"
/>
</div>
</form>
</Modal.Body>
<Modal.Footer>
<Button bsStyle="success" onClick={this.handleSubmit}>Apply</Button>
<Button onClick={this.hideModal}>Close</Button>
</Modal.Footer>
</Modal>
</span>
);
}
}

View File

@ -41,6 +41,9 @@ export default class AppNavBar extends React.PureComponent {
this.setState({ expanded: value }); this.setState({ expanded: value });
} }
render() { render() {
const loggedAndActivated = (this.state.userLoggedIn && this.props.isActivated);
const displayShowsSearch = (this.state.displayShowsSearch && loggedAndActivated);
const displayMoviesSearch = (this.state.displayMoviesSearch && loggedAndActivated);
return ( return (
<div> <div>
<Navbar fluid fixedTop collapseOnSelect expanded={this.state.expanded} onToggle={this.setExpanded}> <Navbar fluid fixedTop collapseOnSelect expanded={this.state.expanded} onToggle={this.setExpanded}>
@ -51,20 +54,24 @@ export default class AppNavBar extends React.PureComponent {
<Navbar.Toggle /> <Navbar.Toggle />
</Navbar.Header> </Navbar.Header>
<Navbar.Collapse> <Navbar.Collapse>
{this.state.userLoggedIn && {loggedAndActivated &&
<MoviesDropdown /> <MoviesDropdown />
} }
{this.state.userLoggedIn && {loggedAndActivated &&
<ShowsDropdown /> <ShowsDropdown />
} }
{this.state.userLoggedIn && {loggedAndActivated &&
<WishlistDropdown /> <WishlistDropdown />
} }
{this.state.userLoggedIn && {loggedAndActivated &&
<Torrents torrentsCount={this.props.torrentCount} /> <Torrents torrentsCount={this.props.torrentCount} />
} }
<UserDropdown username={this.props.username} isAdmin={this.props.isAdmin} /> <UserDropdown
{(this.state.displayMoviesSearch && this.state.userLoggedIn) && username={this.props.username}
isAdmin={this.props.isAdmin}
isActivated={this.props.isActivated}
/>
{displayMoviesSearch &&
<Search <Search
placeholder="Search movies" placeholder="Search movies"
router={this.props.router} router={this.props.router}
@ -72,7 +79,7 @@ export default class AppNavBar extends React.PureComponent {
setExpanded={this.setExpanded} setExpanded={this.setExpanded}
/> />
} }
{(this.state.displayShowsSearch && this.state.userLoggedIn) && {displayShowsSearch &&
<Search <Search
placeholder="Search shows" placeholder="Search shows"
router={this.props.router} router={this.props.router}
@ -152,9 +159,11 @@ function UserDropdown(props) {
<MenuItem>Admin Panel</MenuItem> <MenuItem>Admin Panel</MenuItem>
</LinkContainer> </LinkContainer>
} }
<LinkContainer to="/users/edit"> {props.isActivated &&
<MenuItem>Edit</MenuItem> <LinkContainer to="/users/edit">
</LinkContainer> <MenuItem>Edit</MenuItem>
</LinkContainer>
}
<LinkContainer to="/users/logout"> <LinkContainer to="/users/logout">
<MenuItem>Logout</MenuItem> <MenuItem>Logout</MenuItem>
</LinkContainer> </LinkContainer>

View File

@ -0,0 +1,16 @@
import React from "react"
function UserActivation(props) {
return (
<div className="container">
<div className="content-fluid">
<div className="col-md-8 col-md-offset-2 col-xs-12">
<h2>Waiting for activation</h2>
<hr />
<h3>Hang tight! Your user will soon be activated by the administrators of this site.</h3>
</div>
</div>
</div>
);
}
export default UserActivation;

View File

@ -4,6 +4,12 @@ import { bindActionCreators } from "redux"
import { userSignUp } from "../../actions/users" import { userSignUp } from "../../actions/users"
function mapStateToProps(state) {
return {
isLogged: state.userStore.get("isLogged"),
};
}
const mapDispatchToProps = (dispatch) => const mapDispatchToProps = (dispatch) =>
bindActionCreators({ userSignUp }, dispatch) bindActionCreators({ userSignUp }, dispatch)
@ -20,6 +26,13 @@ class UserSignUp extends React.PureComponent {
"password_confirm": this.refs.passwordConfirm.value, "password_confirm": this.refs.passwordConfirm.value,
}); });
} }
componentWillReceiveProps(nextProps) {
if (!nextProps.isLogged) {
return
}
// Redirect home
nextProps.router.push("/");
}
render() { render() {
return ( return (
<div className="container"> <div className="container">
@ -53,4 +66,4 @@ class UserSignUp extends React.PureComponent {
); );
} }
} }
export default connect(null, mapDispatchToProps)(UserSignUp); export default connect(mapStateToProps, mapDispatchToProps)(UserSignUp);

View File

@ -2,10 +2,12 @@ import { Map, List, fromJS } from "immutable"
const defaultState = Map({ const defaultState = Map({
"users": List(), "users": List(),
"stats": Map({}),
}); });
const handlers = { const handlers = {
"ADMIN_LIST_USERS_FULFILLED": (state, action) => state.set("users", fromJS(action.payload.response.data)), "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) => export default (state = defaultState, action) =>

View File

@ -7,6 +7,7 @@ const defaultState = Map({
loading: false, loading: false,
username: "", username: "",
isAdmin: false, isAdmin: false,
isActivated: false,
isLogged: false, isLogged: false,
polochonToken: "", polochonToken: "",
polochonUrl: "", polochonUrl: "",
@ -46,6 +47,7 @@ function updateFromToken(state, token) {
userLoading: false, userLoading: false,
isLogged: true, isLogged: true,
isAdmin: decodedToken.isAdmin, isAdmin: decodedToken.isAdmin,
isActivated: decodedToken.isActivated,
username: decodedToken.username, username: decodedToken.username,
})) }))
} }

View File

@ -50,6 +50,9 @@ export function request(eventPrefix, promise, callbackEvents = null, mainPayload
}) })
if (callbackEvents) { if (callbackEvents) {
for (let event of callbackEvents) { for (let event of callbackEvents) {
if (typeof event === 'function') {
event = event();
}
dispatch(event); dispatch(event);
} }
} }

View File

@ -3,15 +3,16 @@ import ShowList from "./components/shows/list"
import ShowDetails from "./components/shows/details" import ShowDetails from "./components/shows/details"
import UserLoginForm from "./components/users/login" import UserLoginForm from "./components/users/login"
import UserEdit from "./components/users/edit" import UserEdit from "./components/users/edit"
import UserActivation from "./components/users/activation"
import UserSignUp from "./components/users/signup" import UserSignUp from "./components/users/signup"
import TorrentList from "./components/torrents/list" import TorrentList from "./components/torrents/list"
import AdminView from "./components/admins/users" import AdminPanel from "./components/admins/panel"
import { fetchTorrents } from "./actions/torrents" import { fetchTorrents } from "./actions/torrents"
import { userLogout, getUserInfos } from "./actions/users" import { userLogout, getUserInfos } from "./actions/users"
import { fetchMovies, getMovieExploreOptions } from "./actions/movies" import { fetchMovies, getMovieExploreOptions } from "./actions/movies"
import { fetchShows, fetchShowDetails, getShowExploreOptions } from "./actions/shows" import { fetchShows, fetchShowDetails, getShowExploreOptions } from "./actions/shows"
import { getUsers } from "./actions/admins" import { getUsers, getStats } from "./actions/admins"
import store from "./store" import store from "./store"
@ -42,15 +43,20 @@ function isLoggedIn() {
return false return false
} }
function isActivated() {
const state = store.getState();
return state.userStore.get("isActivated");
}
var pollingTorrentsId; var pollingTorrentsId;
const loginCheck = function(nextState, replace, next, f = null) { const loginCheck = function(nextState, replace, next, f = null) {
const loggedIn = isLoggedIn(); const loggedIn = isLoggedIn();
if (!loggedIn) { if (!loggedIn) {
replace("/users/login"); replace("/users/login");
} else if (!isActivated()) {
replace("/users/activation");
} else { } else {
if (f) { if (f) { f(); }
f();
}
// Poll torrents once logged // Poll torrents once logged
if (!pollingTorrentsId) { if (!pollingTorrentsId) {
@ -82,7 +88,14 @@ export default function getRoutes(App) {
childRoutes: [ childRoutes: [
{ {
path: "/users/signup", 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", path: "/users/login",
@ -104,6 +117,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", path: "/users/logout",
onEnter: function(nextState, replace, next) { onEnter: function(nextState, replace, next) {
@ -223,10 +250,11 @@ export default function getRoutes(App) {
}, },
{ {
path: "/admin", path: "/admin",
component: AdminView, component: AdminPanel,
onEnter: function(nextState, replace, next) { onEnter: function(nextState, replace, next) {
adminCheck(nextState, replace, next, function() { adminCheck(nextState, replace, next, function() {
store.dispatch(getUsers()); store.dispatch(getUsers());
store.dispatch(getStats());
}); });
}, },
}, },

View File

@ -2,6 +2,7 @@
@import "~bootswatch/superhero/variables.less"; @import "~bootswatch/superhero/variables.less";
@import "~bootswatch/superhero/bootswatch.less"; @import "~bootswatch/superhero/bootswatch.less";
@import "~font-awesome/less/font-awesome.less"; @import "~font-awesome/less/font-awesome.less";
@import "~react-bootstrap-toggle/src/bootstrap2-toggle.css";
body { body {
padding-top: @navbar-height + 10px; padding-top: @navbar-height + 10px;
@ -75,3 +76,8 @@ div.sweet-alert > h2 {
.player-modal { .player-modal {
width: 90%; width: 90%;
} }
.admin-edit-user-modal {
margin-left: 3px;
margin-right: 3px;
}

View File

@ -60,4 +60,6 @@ func setupRoutes(env *web.Env) {
// Admin routes // Admin routes
env.Handle("/admins/users", admin.GetUsersHandler).WithRole(users.AdminRole).Methods("GET") 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")
} }

View File

@ -1125,6 +1125,14 @@ core-util-is@~1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 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: cryptiles@2.x.x:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
@ -1613,6 +1621,18 @@ fbjs@^0.8.4:
promise "^7.1.1" promise "^7.1.1"
ua-parser-js "^0.7.9" 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: figures@^1.3.5:
version "1.7.0" version "1.7.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" 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: dependencies:
js-tokens "^2.0.0" 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: loud-rejection@^1.0.0:
version "1.6.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" 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" version "4.1.0"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0" 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: object.omit@^2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
@ -3406,6 +3436,13 @@ promise@^7.1.1:
dependencies: dependencies:
asap "~2.0.3" 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: prr@~0.0.0:
version "0.0.0" version "0.0.0"
resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
@ -3477,10 +3514,27 @@ react-bootstrap-sweetalert:
dependencies: dependencies:
object-assign "^4.1.0" 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: react-dom@^15.3.2:
version "15.3.2" version "15.3.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.3.2.tgz#c46b0aa5380d7b838e7a59c4a7beff2ed315531f" 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: react-infinite-scroller:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/react-infinite-scroller/-/react-infinite-scroller-1.0.4.tgz#cb171113c4c8ee6aa44669392a55ad50938e2028" 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" loose-envify "^1.1.0"
object-assign "^4.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: read-pkg-up@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" 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" version "1.0.1"
resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" 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: sha.js@2.2.6:
version "2.2.6" version "2.2.6"
resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.2.6.tgz#17ddeddc5f722fb66501658895461977867315ba" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.2.6.tgz#17ddeddc5f722fb66501658895461977867315ba"