mirror of
https://github.com/idanoo/GoScrobble.git
synced 2024-11-24 17:35:16 +00:00
0.0.3
- Clean up login/redirect flow - Add redirect when not authed on other endpoints - Add GET /stats endpoint for overal stats
This commit is contained in:
parent
5fd9d41069
commit
038823055a
@ -3,7 +3,7 @@ stages:
|
|||||||
- bundle
|
- bundle
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
VERSION: 0.0.2
|
VERSION: 0.0.3
|
||||||
|
|
||||||
build-go:
|
build-go:
|
||||||
image: golang:1.16.2
|
image: golang:1.16.2
|
||||||
|
@ -22,7 +22,7 @@ Copy .env.example to .env and set variables. You can use https://www.grc.com/pas
|
|||||||
cp .env.example .env # Fill in the blanks
|
cp .env.example .env # Fill in the blanks
|
||||||
go mod tidy
|
go mod tidy
|
||||||
CGO_ENABLED=0 go run cmd/go-scrobble/*.go
|
CGO_ENABLED=0 go run cmd/go-scrobble/*.go
|
||||||
# In another terminal set web/.env.development
|
# In another terminal cp web/.env.example web/.env.development and set vars
|
||||||
cd web && npm install && npm start --env development
|
cd web && npm install && npm start --env development
|
||||||
|
|
||||||
|
|
||||||
@ -32,6 +32,7 @@ Access dev frontend @ http://127.0.0.1:3000 + API @ http://127.0.0.1:42069/api/v
|
|||||||
We need to build NPM package, and then ship web/build with the binary.
|
We need to build NPM package, and then ship web/build with the binary.
|
||||||
|
|
||||||
cp .env.example .env # Fill in the blanks
|
cp .env.example .env # Fill in the blanks
|
||||||
|
cp web/.env.example web/.env.production
|
||||||
cd web npm install --production && npm run build --env production
|
cd web npm install --production && npm run build --env production
|
||||||
go build -o goscrobble cmd/go-scrobble/*.go
|
go build -o goscrobble cmd/go-scrobble/*.go
|
||||||
./goscrobble
|
./goscrobble
|
||||||
|
@ -1,3 +1,11 @@
|
|||||||
|
# 0.0.4
|
||||||
|
- Display stats on homepage
|
||||||
|
|
||||||
|
# 0.0.3
|
||||||
|
- Clean up login/redirect flow
|
||||||
|
- Add redirect when not authed on other endpoints
|
||||||
|
- Add GET /stats endpoint for overal stats
|
||||||
|
|
||||||
# 0.0.2
|
# 0.0.2
|
||||||
- Login flow working..
|
- Login flow working..
|
||||||
- Jellyfin scrobble working
|
- Jellyfin scrobble working
|
||||||
|
@ -54,6 +54,7 @@ func HandleRequests(port string) {
|
|||||||
// No Auth
|
// No Auth
|
||||||
v1.HandleFunc("/register", limitMiddleware(handleRegister, heavyLimiter)).Methods("POST")
|
v1.HandleFunc("/register", limitMiddleware(handleRegister, heavyLimiter)).Methods("POST")
|
||||||
v1.HandleFunc("/login", limitMiddleware(handleLogin, standardLimiter)).Methods("POST")
|
v1.HandleFunc("/login", limitMiddleware(handleLogin, standardLimiter)).Methods("POST")
|
||||||
|
v1.HandleFunc("/stats", handleStats).Methods("GET")
|
||||||
|
|
||||||
// This just prevents it serving frontend stuff over /api
|
// This just prevents it serving frontend stuff over /api
|
||||||
r.PathPrefix("/api")
|
r.PathPrefix("/api")
|
||||||
@ -233,6 +234,19 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write(data)
|
w.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleStats - Returns stats for homepage
|
||||||
|
func handleStats(w http.ResponseWriter, r *http.Request) {
|
||||||
|
stats, err := getStats()
|
||||||
|
if err != nil {
|
||||||
|
throwOkError(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
js, _ := json.Marshal(&stats)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(js)
|
||||||
|
}
|
||||||
|
|
||||||
// serveEndpoint - API stuffs
|
// serveEndpoint - API stuffs
|
||||||
func handleIngress(w http.ResponseWriter, r *http.Request, userUuid string) {
|
func handleIngress(w http.ResponseWriter, r *http.Request, userUuid string) {
|
||||||
bodyJson, err := decodeJson(r.Body)
|
bodyJson, err := decodeJson(r.Body)
|
||||||
|
94
internal/goscrobble/stats.go
Normal file
94
internal/goscrobble/stats.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package goscrobble
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StatsRequest struct {
|
||||||
|
Users int `json:"users"`
|
||||||
|
Scrobbles int `json:"scrobbles"`
|
||||||
|
Tracks int `json:"songs"`
|
||||||
|
Artists int `json:"artists"`
|
||||||
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStats() (StatsRequest, error) {
|
||||||
|
js := getRedisVal("stats")
|
||||||
|
statsReq := StatsRequest{}
|
||||||
|
var err error
|
||||||
|
if js != "" {
|
||||||
|
// If cached, deserialize and return
|
||||||
|
err = json.Unmarshal([]byte(js), &statsReq)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error unmarshalling stats json: %+v", err)
|
||||||
|
return statsReq, errors.New("Error fetching stats")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if older than 5 min - we want to update async for the next caller
|
||||||
|
now := time.Now()
|
||||||
|
if now.Sub(statsReq.LastUpdated) > time.Duration(5)*time.Minute {
|
||||||
|
go goFetchStats()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If not cached, pull data then return
|
||||||
|
statsReq, err = fetchStats()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error fetching stats: %+v", err)
|
||||||
|
return statsReq, errors.New("Error fetching stats")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return statsReq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// goFetchStats - Async call
|
||||||
|
func goFetchStats() {
|
||||||
|
_, _ = fetchStats()
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchStats() (StatsRequest, error) {
|
||||||
|
statsReq := StatsRequest{}
|
||||||
|
var err error
|
||||||
|
|
||||||
|
statsReq.Users, err = getDbCount("SELECT COUNT(*) FROM `users` WHERE `active` = 1")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to fetch user count: %+v", err)
|
||||||
|
return statsReq, errors.New("Failed to fetch stats")
|
||||||
|
}
|
||||||
|
|
||||||
|
statsReq.Scrobbles, err = getDbCount("SELECT COUNT(*) FROM `scrobbles`")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to fetch scrobble count: %+v", err)
|
||||||
|
return statsReq, errors.New("Failed to fetch stats")
|
||||||
|
}
|
||||||
|
|
||||||
|
statsReq.Tracks, err = getDbCount("SELECT COUNT(*) FROM `tracks`")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to fetch track count: %+v", err)
|
||||||
|
return statsReq, errors.New("Failed to fetch stats")
|
||||||
|
}
|
||||||
|
|
||||||
|
statsReq.Artists, err = getDbCount("SELECT COUNT(*) FROM `artists`")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to fetch artist count: %+v", err)
|
||||||
|
return statsReq, errors.New("Failed to fetch stats")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark the time this was last updated
|
||||||
|
statsReq.LastUpdated = time.Now()
|
||||||
|
|
||||||
|
b, err := json.Marshal(statsReq)
|
||||||
|
if err != nil {
|
||||||
|
return statsReq, errors.New("Failed to fetch stats")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = setRedisVal("stats", string(b))
|
||||||
|
if err != nil {
|
||||||
|
return statsReq, errors.New("Failed to fetch stats")
|
||||||
|
}
|
||||||
|
|
||||||
|
return statsReq, nil
|
||||||
|
}
|
2
web/.env.example
Normal file
2
web/.env.example
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
REACT_APP_API_URL=http://127.0.0.1:42069/api/v1/
|
||||||
|
REACT_APP_REGISTRATION_DISABLED=false
|
29
web/src/Actions/api.js
Normal file
29
web/src/Actions/api.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import ApiService from "../Services/api.service";
|
||||||
|
|
||||||
|
export const getStats = () => () => {
|
||||||
|
return ApiService.getStats().then(
|
||||||
|
(data) => {
|
||||||
|
console.log(data);
|
||||||
|
if (data.error) {
|
||||||
|
toast.error(data.error)
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
const message =
|
||||||
|
(error.response &&
|
||||||
|
error.response.data &&
|
||||||
|
error.response.data.message) ||
|
||||||
|
error.message ||
|
||||||
|
error.toString();
|
||||||
|
console.log(message);
|
||||||
|
|
||||||
|
toast.error(message);
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -3,20 +3,30 @@ import {
|
|||||||
REGISTER_FAIL,
|
REGISTER_FAIL,
|
||||||
LOGIN_SUCCESS,
|
LOGIN_SUCCESS,
|
||||||
LOGIN_FAIL,
|
LOGIN_FAIL,
|
||||||
SET_MESSAGE,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { toast } from 'react-toastify'
|
|
||||||
|
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import jwt from 'jwt-decode'
|
||||||
import AuthService from "../Services/auth.service";
|
import AuthService from "../Services/auth.service";
|
||||||
|
|
||||||
export const register = (username, email, password) => (dispatch) => {
|
export const register = (username, email, password) => (dispatch) => {
|
||||||
return AuthService.register(username, email, password).then(
|
return AuthService.register(username, email, password).then(
|
||||||
(response) => {
|
(data) => {
|
||||||
|
if (data.message) {
|
||||||
|
toast.success('Successfully registered. Please sign in');
|
||||||
|
dispatch({
|
||||||
|
type: REGISTER_SUCCESS,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error(data.error ? data.error: 'An Unknown Error has occurred')
|
||||||
dispatch({
|
dispatch({
|
||||||
type: REGISTER_SUCCESS,
|
type: REGISTER_FAIL,
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.reject();
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
const message =
|
const message =
|
||||||
@ -26,13 +36,10 @@ import {
|
|||||||
error.message ||
|
error.message ||
|
||||||
error.toString();
|
error.toString();
|
||||||
|
|
||||||
dispatch({
|
toast.error(message);
|
||||||
type: REGISTER_FAIL,
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: SET_MESSAGE,
|
type: REGISTER_FAIL,
|
||||||
payload: message,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
@ -45,9 +52,11 @@ import {
|
|||||||
(data) => {
|
(data) => {
|
||||||
if (data.token) {
|
if (data.token) {
|
||||||
toast.success('Login Success');
|
toast.success('Login Success');
|
||||||
|
let user = jwt(data.token)
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: LOGIN_SUCCESS,
|
type: LOGIN_SUCCESS,
|
||||||
payload: { user: data },
|
payload: { jwt: data.token, sub: user.sub, exp: user.exp },
|
||||||
});
|
});
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
@ -71,17 +80,17 @@ import {
|
|||||||
type: LOGIN_FAIL,
|
type: LOGIN_FAIL,
|
||||||
});
|
});
|
||||||
|
|
||||||
// dispatch({
|
|
||||||
// type: SET_MESSAGE,
|
|
||||||
// payload: message,
|
|
||||||
// });
|
|
||||||
|
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const logout = () => () => {
|
export const logout = () => (dispatch) => {
|
||||||
AuthService.logout();
|
AuthService.logout();
|
||||||
|
|
||||||
|
// dispatch({
|
||||||
|
// type: LOGOUT,
|
||||||
|
// });
|
||||||
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
|
13
web/src/Actions/eventBus.js
Normal file
13
web/src/Actions/eventBus.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const eventBus = {
|
||||||
|
on(event, callback) {
|
||||||
|
document.addEventListener(event, (e) => callback(e.detail));
|
||||||
|
},
|
||||||
|
dispatch(event, data) {
|
||||||
|
document.dispatchEvent(new CustomEvent(event, { detail: data }));
|
||||||
|
},
|
||||||
|
remove(event, callback) {
|
||||||
|
document.removeEventListener(event, callback);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default eventBus
|
@ -11,8 +11,6 @@ import Register from './Pages/Register';
|
|||||||
import Navigation from './Components/Navigation';
|
import Navigation from './Components/Navigation';
|
||||||
|
|
||||||
import { logout } from './Actions/auth';
|
import { logout } from './Actions/auth';
|
||||||
import { clearMessage } from './Actions/message';
|
|
||||||
import { history } from './Helpers/history';
|
|
||||||
import { Route, Switch, withRouter } from 'react-router-dom';
|
import { Route, Switch, withRouter } from 'react-router-dom';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
@ -37,10 +35,6 @@ class App extends Component {
|
|||||||
// exact="true".. it has to be a bool :|
|
// exact="true".. it has to be a bool :|
|
||||||
true: true,
|
true: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
history.listen((location) => {
|
|
||||||
props.dispatch(clearMessage()); // clear message when changing location
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
0
web/src/Components/HomeBanner.css
Normal file
0
web/src/Components/HomeBanner.css
Normal file
44
web/src/Components/HomeBanner.js
Normal file
44
web/src/Components/HomeBanner.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import '../App.css';
|
||||||
|
import './HomeBanner.css';
|
||||||
|
import { getStats } from '../Actions/api';
|
||||||
|
|
||||||
|
class HomeBanner extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isLoading: true,
|
||||||
|
userCount: 0,
|
||||||
|
scrobbleCount: 0,
|
||||||
|
trackCount: 0,
|
||||||
|
artistCount: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
getStats()
|
||||||
|
// .then((data) => {
|
||||||
|
// this.setState({
|
||||||
|
// loading: false,
|
||||||
|
// userCount: data.users,
|
||||||
|
// scrobbleCount: data.scrobbles,
|
||||||
|
// trackCount: data.tracks,
|
||||||
|
// artistCount: data.artists,
|
||||||
|
// });
|
||||||
|
// })
|
||||||
|
// .catch(() => {
|
||||||
|
// this.setState({
|
||||||
|
// loading: false
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HomeBanner;
|
@ -5,6 +5,12 @@ import logo from '../logo.png';
|
|||||||
import './Navigation.css';
|
import './Navigation.css';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { logout } from '../Actions/auth';
|
import { logout } from '../Actions/auth';
|
||||||
|
import eventBus from "../Actions/eventBus";
|
||||||
|
|
||||||
|
import {
|
||||||
|
LOGIN_SUCCESS,
|
||||||
|
LOGOUT,
|
||||||
|
} from "../Actions/types";
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
'Home',
|
'Home',
|
||||||
@ -24,32 +30,62 @@ class Navigation extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const isLoggedIn = this.props.isLoggedIn;
|
const { isLoggedIn } = this.props;
|
||||||
|
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
this.setState({
|
this.setState({
|
||||||
isLoggedIn: true,
|
isLoggedIn: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventBus.on(LOGIN_SUCCESS, () =>
|
||||||
|
this.setState({ isLoggedIn: true })
|
||||||
|
);
|
||||||
|
|
||||||
|
eventBus.on(LOGOUT, () =>
|
||||||
|
this.setState({ isLoggedIn: false })
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
eventBus.remove(LOGIN_SUCCESS);
|
||||||
|
eventBus.remove(LOGOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
_handleClick(menuItem) {
|
_handleClick(menuItem) {
|
||||||
this.setState({ active: menuItem });
|
this.setState({ active: menuItem });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const activeStyle = { color: '#FFFFFF' };
|
const activeStyle = { color: '#FFFFFF' };
|
||||||
|
|
||||||
const renderAuthButtons = () => {
|
const renderAuthButtons = () => {
|
||||||
if (this.state.isLoggedIn) {
|
if (this.state.isLoggedIn) {
|
||||||
return <div className="navLinkLogin">
|
return <div className="navLinkLogin">
|
||||||
<Link to="/profile" className="navLink">Profile</Link>
|
<Link
|
||||||
|
to="/profile"
|
||||||
|
style={this.state.active === "profile" ? activeStyle : {}}
|
||||||
|
onClick={this._handleClick.bind(this, "profile")}
|
||||||
|
className="navLink"
|
||||||
|
>Profile</Link>
|
||||||
<Link to="/" className="navLink" onClick={logout()}>Logout</Link>
|
<Link to="/" className="navLink" onClick={logout()}>Logout</Link>
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
return <div className="navLinkLogin">
|
return <div className="navLinkLogin">
|
||||||
<Link to="/login" className="navLink">Login</Link>
|
<Link
|
||||||
<Link to="/register" className="navLink" history={this.props.history}>Register</Link>
|
to="/login"
|
||||||
|
style={this.state.active === "login" ? activeStyle : {}}
|
||||||
|
onClick={this._handleClick.bind(this, "login")}
|
||||||
|
className="navLink"
|
||||||
|
>Login</Link>
|
||||||
|
<Link
|
||||||
|
to="/register"
|
||||||
|
className="navLink"
|
||||||
|
style={this.state.active === "register" ? activeStyle : {}}
|
||||||
|
onClick={this._handleClick.bind(this, "register")}
|
||||||
|
history={this.props.history}
|
||||||
|
>Register</Link>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -89,7 +125,7 @@ class Navigation extends Component {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Navbar color="dark" dark fixed="top">
|
<Navbar color="dark" dark fixed="top">
|
||||||
<NavbarBrand href="/" className="mr-auto"><img src={logo} className="nav-logo" alt="logo" /> GoScrobble</NavbarBrand>
|
<NavbarBrand className="mr-auto"><img src={logo} className="nav-logo" alt="logo" /> GoScrobble</NavbarBrand>
|
||||||
{renderMenuButtons()}
|
{renderMenuButtons()}
|
||||||
{renderAuthButtons()}
|
{renderAuthButtons()}
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
@ -10,7 +10,6 @@ class Dashboard extends React.Component {
|
|||||||
|
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
history.push("/login")
|
history.push("/login")
|
||||||
window.location.reload()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,7 +17,7 @@ class Dashboard extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div className="pageWrapper">
|
<div className="pageWrapper">
|
||||||
<h1>
|
<h1>
|
||||||
Hai Dashboard!
|
Dashboard!
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import logo from '../logo.png';
|
import logo from '../logo.png';
|
||||||
import '../App.css';
|
import '../App.css';
|
||||||
|
import HomeBanner from '../Components/HomeBanner';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
function Home() {
|
class Home extends React.Component {
|
||||||
return (
|
render() {
|
||||||
|
return (
|
||||||
<div className="App-header">
|
<div className="App-header">
|
||||||
<img src={logo} className="App-logo" alt="logo" />
|
<img src={logo} className="App-logo" alt="logo" />
|
||||||
<p>
|
<p>
|
||||||
@ -16,8 +19,10 @@ function Home() {
|
|||||||
>
|
>
|
||||||
gitlab.com/idanoo/go-scrobble
|
gitlab.com/idanoo/go-scrobble
|
||||||
</a>
|
</a>
|
||||||
|
<HomeBanner />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Home;
|
export default Home;
|
||||||
|
@ -6,6 +6,8 @@ import { Formik, Form, Field } from 'formik';
|
|||||||
import ScaleLoader from 'react-spinners/ScaleLoader';
|
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { login } from '../Actions/auth';
|
import { login } from '../Actions/auth';
|
||||||
|
import eventBus from "../Actions/eventBus";
|
||||||
|
import { LOGIN_SUCCESS } from '../Actions/types';
|
||||||
|
|
||||||
class Login extends React.Component {
|
class Login extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -13,6 +15,14 @@ class Login extends React.Component {
|
|||||||
this.state = {username: '', password: '', loading: false};
|
this.state = {username: '', password: '', loading: false};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { history, isLoggedIn } = this.props;
|
||||||
|
|
||||||
|
if (isLoggedIn) {
|
||||||
|
history.push("/dashboard")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleLogin(values) {
|
handleLogin(values) {
|
||||||
this.setState({loading: true});
|
this.setState({loading: true});
|
||||||
|
|
||||||
@ -22,9 +32,11 @@ class Login extends React.Component {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
|
isLoggedIn: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
eventBus.dispatch(LOGIN_SUCCESS, { isLoggedIn: true });
|
||||||
history.push("/dashboard");
|
history.push("/dashboard");
|
||||||
window.location.reload();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -82,10 +94,8 @@ class Login extends React.Component {
|
|||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
const { isLoggedIn } = state.auth;
|
const { isLoggedIn } = state.auth;
|
||||||
const { message } = state.message;
|
|
||||||
return {
|
return {
|
||||||
isLoggedIn,
|
isLoggedIn
|
||||||
message
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,12 +5,10 @@ import { connect } from 'react-redux';
|
|||||||
|
|
||||||
class Profile extends React.Component {
|
class Profile extends React.Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { history } = this.props;
|
const { history, isLoggedIn } = this.props;
|
||||||
const isLoggedIn = this.props.isLoggedIn;
|
|
||||||
|
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
history.push("/login")
|
history.push("/login")
|
||||||
window.location.reload()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,86 +1,42 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import '../App.css';
|
import '../App.css';
|
||||||
import './Login.css';
|
import './Login.css';
|
||||||
import { Button } from 'reactstrap';
|
import { Button, Form } from 'reactstrap';
|
||||||
import ScaleLoader from "react-spinners/ScaleLoader";
|
import ScaleLoader from "react-spinners/ScaleLoader";
|
||||||
import { withRouter } from 'react-router-dom'
|
import { register } from '../Actions/auth';
|
||||||
|
import { Formik, Field } from 'formik';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
class Register extends React.Component {
|
class Register extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {username: '', email: '', password: '', passwordconfirm: '', loading: false};
|
this.state = {username: '', email: '', password: '', passwordconfirm: '', loading: false};
|
||||||
this.handleUsernameChange = this.handleUsernameChange.bind(this);
|
|
||||||
this.handleEmailChange = this.handleEmailChange.bind(this);
|
|
||||||
this.handlePasswordChange = this.handlePasswordChange.bind(this);
|
|
||||||
this.handlePasswordConfirmChange = this.handlePasswordConfirmChange.bind(this);
|
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUsernameChange(event) {
|
componentDidMount() {
|
||||||
this.setState({username: event.target.value});
|
const { history, isLoggedIn } = this.props;
|
||||||
}
|
|
||||||
|
|
||||||
handleEmailChange(event) {
|
if (isLoggedIn) {
|
||||||
this.setState({email: event.target.value});
|
history.push("/dashboard")
|
||||||
}
|
|
||||||
|
|
||||||
handlePasswordChange(event) {
|
|
||||||
this.setState({password: event.target.value});
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePasswordConfirmChange(event) {
|
|
||||||
this.setState({passwordconfirm: event.target.value});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSubmit(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
if (this.state.password !== this.state.passwordconfirm) {
|
|
||||||
this.props.addToast('Passwords do not match', { appearance: 'error' });
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// if (this.state.password.len < 8) {
|
handleRegister(values) {
|
||||||
// this.props.addToast('Password must be at least 8 characters', { appearance: 'error' });
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
this.setState({loading: true});
|
this.setState({loading: true});
|
||||||
const requestOptions = {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
timeout: 5000,
|
|
||||||
body: JSON.stringify({
|
|
||||||
username: this.state.username,
|
|
||||||
email: this.state.email,
|
|
||||||
password: this.state.password,
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
const apiUrl = process.env.REACT_APP_API_URL + '/api/v1/register';
|
const { dispatch, history } = this.props;
|
||||||
console.log(apiUrl);
|
|
||||||
fetch(apiUrl, requestOptions)
|
dispatch(register(values.username, values.email, values.password))
|
||||||
.then((response) => {
|
.then(() => {
|
||||||
if (response.status === 429) {
|
this.setState({
|
||||||
this.props.addToast("Rate limited. Please try again soon", { appearance: 'error' });
|
loading: false,
|
||||||
return "{}"
|
});
|
||||||
} else {
|
history.push("/login");
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.then((function(data) {
|
|
||||||
console.log(data);
|
|
||||||
if (data.error) {
|
|
||||||
this.props.addToast(data.error, { appearance: 'error' });
|
|
||||||
} else if (data.message) {
|
|
||||||
this.props.addToast(data.message, { appearance: 'success' });
|
|
||||||
this.props.history.push('/login')
|
|
||||||
}
|
|
||||||
this.setState({loading: false});
|
|
||||||
}).bind(this))
|
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.props.addToast('Error submitting form. Please try again', { appearance: 'error' });
|
this.setState({
|
||||||
this.setState({loading: false});
|
loading: false
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,57 +54,57 @@ class Register extends React.Component {
|
|||||||
Register
|
Register
|
||||||
</h1>
|
</h1>
|
||||||
<div className="loginBody">
|
<div className="loginBody">
|
||||||
<form onSubmit={this.handleSubmit}>
|
<Formik
|
||||||
<label>
|
initialValues={{ username: '', email: '', password: '', passwordconfirm: '' }}
|
||||||
Username*<br/>
|
onSubmit={async values => this.handleRegister(values)}>
|
||||||
<input
|
<Form>
|
||||||
type="text"
|
<label>
|
||||||
required={trueBool}
|
Username*<br/>
|
||||||
className="loginFields"
|
<Field
|
||||||
value={this.state.username}
|
name="username"
|
||||||
onChange={this.handleUsernameChange}
|
type="text"
|
||||||
/>
|
required={trueBool}
|
||||||
</label>
|
className="loginFields"
|
||||||
<br/>
|
/>
|
||||||
<label>
|
</label>
|
||||||
Email<br/>
|
<br/>
|
||||||
<input
|
<label>
|
||||||
type="email"
|
Email<br/>
|
||||||
className="loginFields"
|
<Field
|
||||||
value={this.state.email}
|
name="email"
|
||||||
onChange={this.handleEmailChange}
|
type="email"
|
||||||
/>
|
className="loginFields"
|
||||||
</label>
|
/>
|
||||||
<br/>
|
</label>
|
||||||
<label>
|
<br/>
|
||||||
Password*<br/>
|
<label>
|
||||||
<input
|
Password*<br/>
|
||||||
type="password"
|
<Field
|
||||||
required={trueBool}
|
name="password"
|
||||||
className="loginFields"
|
type="password"
|
||||||
value={this.state.password}
|
required={trueBool}
|
||||||
onChange={this.handlePasswordChange}
|
className="loginFields"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<br/>
|
<br/>
|
||||||
<label>
|
<label>
|
||||||
Password*<br/>
|
Confirm Password*<br/>
|
||||||
<input
|
<Field
|
||||||
type="password"
|
name="passwordconfirm"
|
||||||
required={trueBool}
|
type="password"
|
||||||
className="loginFields"
|
required={trueBool}
|
||||||
value={this.state.passwordconfirm}
|
className="loginFields"
|
||||||
onChange={this.handlePasswordConfirmChange}
|
/>
|
||||||
/>
|
</label>
|
||||||
</label>
|
<br/><br/>
|
||||||
<br/><br/>
|
<Button
|
||||||
<Button
|
color="primary"
|
||||||
color="primary"
|
type="submit"
|
||||||
type="submit"
|
className="loginButton"
|
||||||
className="loginButton"
|
disabled={this.state.loading}
|
||||||
disabled={this.state.loading}
|
>{this.state.loading ? <ScaleLoader color="#FFF" size={35} /> : "Register"}</Button>
|
||||||
>{this.state.loading ? <ScaleLoader color="#FFF" size={35} /> : "Register"}</Button>
|
</Form>
|
||||||
</form>
|
</Formik>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -157,4 +113,11 @@ class Register extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(Register);
|
function mapStateToProps(state) {
|
||||||
|
const { isLoggedIn } = state.auth;
|
||||||
|
return {
|
||||||
|
isLoggedIn
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(Register);
|
||||||
|
@ -6,11 +6,11 @@ import {
|
|||||||
LOGOUT,
|
LOGOUT,
|
||||||
} from "../Actions/types";
|
} from "../Actions/types";
|
||||||
|
|
||||||
const jwt = localStorage.getItem("jwt");
|
const user = JSON.parse(localStorage.getItem('user'));
|
||||||
|
|
||||||
const initialState = jwt
|
const initialState = user
|
||||||
? { isLoggedIn: true, jwt }
|
? { isLoggedIn: true, user: user }
|
||||||
: { isLoggedIn: false, jwt };
|
: { isLoggedIn: false, user: null };
|
||||||
|
|
||||||
export default function authReducer(state = initialState, action) {
|
export default function authReducer(state = initialState, action) {
|
||||||
const { type, payload } = action;
|
const { type, payload } = action;
|
||||||
@ -30,13 +30,16 @@ import {
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
isLoggedIn: true,
|
isLoggedIn: true,
|
||||||
user: payload.user,
|
user: {
|
||||||
|
jwt: payload.jwt,
|
||||||
|
uuid: payload.sub,
|
||||||
|
exp: payload.exp,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
case LOGIN_FAIL:
|
case LOGIN_FAIL:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
user: null,
|
|
||||||
};
|
};
|
||||||
case LOGOUT:
|
case LOGOUT:
|
||||||
return {
|
return {
|
||||||
|
13
web/src/Services/api.service.js
Normal file
13
web/src/Services/api.service.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
class ApiService {
|
||||||
|
async getStats() {
|
||||||
|
return axios.get(process.env.REACT_APP_API_URL + "stats")
|
||||||
|
.then((response) => {
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ApiService();
|
@ -1,8 +1,8 @@
|
|||||||
export default function authHeader() {
|
export default function authHeader() {
|
||||||
const token = JSON.parse(localStorage.getItem('jwt'));
|
const auth = localStorage.getItem('user');
|
||||||
|
|
||||||
if (token) {
|
if (auth && auth.jwt) {
|
||||||
return { Authorization: 'Bearer ' + token };
|
return { Authorization: 'Bearer ' + auth.jwt };
|
||||||
} else {
|
} else {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,13 @@ class AuthService {
|
|||||||
.post(process.env.REACT_APP_API_URL + "login", { username, password })
|
.post(process.env.REACT_APP_API_URL + "login", { username, password })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.data.token) {
|
if (response.data.token) {
|
||||||
let user = jwt(response.data.token)
|
let expandedUser = jwt(response.data.token)
|
||||||
localStorage.setItem("jwt", response.data.token);
|
let user = {
|
||||||
localStorage.setItem("uuid", user.sub);
|
jwt: response.data.token,
|
||||||
localStorage.setItem("exp", user.exp);
|
uuid: expandedUser.sub,
|
||||||
|
exp: expandedUser.exp,
|
||||||
|
}
|
||||||
|
localStorage.setItem('user', JSON.stringify(user))
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
@ -18,9 +21,7 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
localStorage.removeItem("jwt");
|
localStorage.removeItem("user");
|
||||||
localStorage.removeItem("uuid");
|
|
||||||
localStorage.removeItem("exp");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
register(username, email, password) {
|
register(username, email, password) {
|
||||||
|
@ -6,7 +6,6 @@ import { HashRouter } from 'react-router-dom';
|
|||||||
import { ToastContainer } from 'react-toastify';
|
import { ToastContainer } from 'react-toastify';
|
||||||
import 'react-toastify/dist/ReactToastify.min.css'
|
import 'react-toastify/dist/ReactToastify.min.css'
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import store from "./store";
|
import store from "./store";
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
|
Loading…
Reference in New Issue
Block a user