commit
4568b8884e
@ -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",
|
||||||
|
@ -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);
|
||||||
|
1
sql/migration/0005__user_activation.down.sql
Normal file
1
sql/migration/0005__user_activation.down.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users DROP COLUMN activated;
|
3
sql/migration/0005__user_activation.up.sql
Normal file
3
sql/migration/0005__user_activation.up.sql
Normal 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);
|
69
src/internal/admins/stats.go
Normal file
69
src/internal/admins/stats.go
Normal 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)
|
||||||
|
}
|
@ -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")
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -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),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
32
src/public/js/components/admins/panel.js
Normal file
32
src/public/js/components/admins/panel.js
Normal 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);
|
54
src/public/js/components/admins/stats.js
Normal file
54
src/public/js/components/admins/stats.js
Normal 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> - {props.data.torrentCount} total</small>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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>
|
||||||
|
16
src/public/js/components/users/activation.js
Normal file
16
src/public/js/components/users/activation.js
Normal 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;
|
@ -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);
|
||||||
|
@ -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) =>
|
||||||
|
@ -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,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
|
68
yarn.lock
68
yarn.lock
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user