Update readme, add stack+support. Add redis env vars. Add disable registration flag

This commit is contained in:
Daniel Mason 2021-03-28 10:39:08 +13:00
parent dc1a2df968
commit feb2ac37da
Signed by: idanoo
GPG Key ID: 387387CDBC02F132
14 changed files with 367 additions and 100 deletions

View File

@ -3,6 +3,11 @@ MYSQL_USER=
MYSQL_PASS=
MYSQL_DB=
REDIS_URL=
REDIS_DB=
REDIS_PREFIX="gs:"
REDIS_AUTH=""
JWT_SECRET=
JWT_EXPIRY=86400

View File

@ -30,7 +30,6 @@ build-react:
expire_in: 1 day
paths:
- web/build
- web/public
- .env.example
bundle:
@ -41,8 +40,8 @@ bundle:
before_script:
- apk add --no-cache zip tar
script:
- zip -r goscrobble.zip web goscrobble migrations init .env.example
- tar -czf goscrobble.tar.gz web goscrobble migrations init .env.example
- zip -r goscrobble.zip web/build goscrobble migrations init .env.example
- tar -czf goscrobble.tar.gz web/build goscrobble migrations init .env.example
artifacts:
expire_in: 6 months
paths:

View File

@ -1,14 +1,14 @@
# go-scrobble
Golang based music scrobbler. MySQL 8.0+
Golang based music scrobbler.
Currently building on Node V15.X & Go V1.16.X
Stack: Go 1.16+, Node 15+, React 17+, MySQL 8.0+, Redis
There are prebuilt binaries/packages available.
With a prebuilt binary - you will still need the migrations folder + web/build folder on prod.
Copy .env.example to .env and set variables. You can use https://www.grc.com/passwords.htm to generate a JWT_SECRET.
## Setup MySQL
create user 'goscrobble'@'%' identified by 'supersecurepass';
create database goscrobble;
@ -32,3 +32,7 @@ We need to build NPM package, and then ship web/build with the binary.
cd web npm install --production && REACT_APP_API_URL=https://goscrobble.com npm run build
go build -o goscrobble cmd/go-scrobble/*.go
./goscrobble
## Support Development!
Feel free to support hosting and my coffee addiction https://liberapay.com/idanoo

21
docs/config.md Normal file
View File

@ -0,0 +1,21 @@
## FRONTEND VARS
REACT_APP_REGISTRATION_DISABLED=true // Disables registration
REACT_APP_API_URL=https://goscrobble.com // Sets API URL
## BACKEND VARS
MYSQL_HOST= // MySQL Server
MYSQL_USER= // MySQL User
MYSQL_PASS= // MySQL Password
MYSQL_DB= // MySQL Database
REDIS_URL= // Redis host
REDIS_DB=4 // Redis DB
REDIS_PREFIX="gs:" // Redis key prefix
REDIS_AUTH="" // Redis password
JWT_SECRET= // 32+ Char JWT secret
JWT_EXPIRY=86400 // JWT expiry
REVERSE_PROXIES=127.0.0.1 // Comma separated list of servers to ignore for IP logs
PORT=42069 // Server port

140
web/package-lock.json generated
View File

@ -13,8 +13,10 @@
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^12.8.3",
"bootstrap": "^4.6.0",
"formik": "^2.2.6",
"react": "^17.0.2",
"react-bootstrap": "^1.5.2",
"react-cookie": "^4.0.3",
"react-dom": "^17.0.2",
"react-redux": "^7.2.3",
"react-router-dom": "^5.2.0",
@ -22,6 +24,7 @@
"react-toast": "^1.0.1",
"react-toast-notifications": "^2.4.3",
"reactstrap": "^8.9.0",
"redux-persist": "^6.0.0",
"web-vitals": "^1.1.1"
},
"devDependencies": {
@ -3095,6 +3098,11 @@
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.11.tgz",
"integrity": "sha512-2koNhpWm3DgWRp5tpkiJ8JGc1xTn2q0l+jUNUE7oMKXUf5NpI9AIdC4kbjGNFBdHtcxBD18LAksoudAVhFKCjw=="
},
"node_modules/@types/cookie": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz",
"integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow=="
},
"node_modules/@types/eslint": {
"version": "7.2.7",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.7.tgz",
@ -8961,6 +8969,42 @@
"node": ">= 0.12"
}
},
"node_modules/formik": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/formik/-/formik-2.2.6.tgz",
"integrity": "sha512-Kxk2zQRafy56zhLmrzcbryUpMBvT0tal5IvcifK5+4YNGelKsnrODFJ0sZQRMQboblWNym4lAW3bt+tf2vApSA==",
"funding": [
{
"type": "individual",
"url": "https://opencollective.com/formik"
}
],
"dependencies": {
"deepmerge": "^2.1.1",
"hoist-non-react-statics": "^3.3.0",
"lodash": "^4.17.14",
"lodash-es": "^4.17.14",
"react-fast-compare": "^2.0.1",
"tiny-warning": "^1.0.2",
"tslib": "^1.10.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/formik/node_modules/deepmerge": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz",
"integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/formik/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
@ -16241,6 +16285,19 @@
"regenerator-runtime": "^0.13.4"
}
},
"node_modules/react-cookie": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.0.3.tgz",
"integrity": "sha512-cmi6IpdVgTSvjqssqIEvo779Gfqc4uPGHRrKMEdHcqkmGtPmxolGfsyKj95bhdLEKqMdbX8MLBCwezlnhkHK0g==",
"dependencies": {
"@types/hoist-non-react-statics": "^3.0.1",
"hoist-non-react-statics": "^3.0.0",
"universal-cookie": "^4.0.0"
},
"peerDependencies": {
"react": ">= 16.3.0"
}
},
"node_modules/react-dev-utils": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.4.tgz",
@ -16357,6 +16414,11 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz",
"integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew=="
},
"node_modules/react-fast-compare": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
},
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@ -16862,6 +16924,14 @@
"symbol-observable": "^1.2.0"
}
},
"node_modules/redux-persist": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz",
"integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==",
"peerDependencies": {
"redux": ">4.0.0"
}
},
"node_modules/redux-thunk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
@ -19892,6 +19962,15 @@
"node": ">=4"
}
},
"node_modules/universal-cookie": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz",
"integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==",
"dependencies": {
"@types/cookie": "^0.3.3",
"cookie": "^0.4.0"
}
},
"node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
@ -24453,6 +24532,11 @@
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.11.tgz",
"integrity": "sha512-2koNhpWm3DgWRp5tpkiJ8JGc1xTn2q0l+jUNUE7oMKXUf5NpI9AIdC4kbjGNFBdHtcxBD18LAksoudAVhFKCjw=="
},
"@types/cookie": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz",
"integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow=="
},
"@types/eslint": {
"version": "7.2.7",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.7.tgz",
@ -29137,6 +29221,32 @@
"mime-types": "^2.1.12"
}
},
"formik": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/formik/-/formik-2.2.6.tgz",
"integrity": "sha512-Kxk2zQRafy56zhLmrzcbryUpMBvT0tal5IvcifK5+4YNGelKsnrODFJ0sZQRMQboblWNym4lAW3bt+tf2vApSA==",
"requires": {
"deepmerge": "^2.1.1",
"hoist-non-react-statics": "^3.3.0",
"lodash": "^4.17.14",
"lodash-es": "^4.17.14",
"react-fast-compare": "^2.0.1",
"tiny-warning": "^1.0.2",
"tslib": "^1.10.0"
},
"dependencies": {
"deepmerge": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz",
"integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA=="
},
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
@ -34782,6 +34892,16 @@
}
}
},
"react-cookie": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.0.3.tgz",
"integrity": "sha512-cmi6IpdVgTSvjqssqIEvo779Gfqc4uPGHRrKMEdHcqkmGtPmxolGfsyKj95bhdLEKqMdbX8MLBCwezlnhkHK0g==",
"requires": {
"@types/hoist-non-react-statics": "^3.0.1",
"hoist-non-react-statics": "^3.0.0",
"universal-cookie": "^4.0.0"
}
},
"react-dev-utils": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.4.tgz",
@ -34872,6 +34992,11 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz",
"integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew=="
},
"react-fast-compare": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
},
"react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@ -35276,6 +35401,12 @@
"symbol-observable": "^1.2.0"
}
},
"redux-persist": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz",
"integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==",
"requires": {}
},
"redux-thunk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
@ -37681,6 +37812,15 @@
"crypto-random-string": "^1.0.0"
}
},
"universal-cookie": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz",
"integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==",
"requires": {
"@types/cookie": "^0.3.3",
"cookie": "^0.4.0"
}
},
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",

View File

@ -8,8 +8,10 @@
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^12.8.3",
"bootstrap": "^4.6.0",
"formik": "^2.2.6",
"react": "^17.0.2",
"react-bootstrap": "^1.5.2",
"react-cookie": "^4.0.3",
"react-dom": "^17.0.2",
"react-redux": "^7.2.3",
"react-router-dom": "^5.2.0",
@ -17,6 +19,7 @@
"react-toast": "^1.0.1",
"react-toast-notifications": "^2.4.3",
"reactstrap": "^8.9.0",
"redux-persist": "^6.0.0",
"web-vitals": "^1.1.1"
},
"scripts": {

View File

@ -2,14 +2,12 @@ import './App.css';
import Home from './Components/Pages/Home';
import About from './Components/Pages/About';
import Login from './Components/Pages/Login';
import Settings from './Components/Pages/Settings';
import Register from './Components/Pages/Register';
import Navigation from './Components/Navigation';
import { Route, Switch, HashRouter } from 'react-router-dom';
import { Route, Switch } from 'react-router-dom';
import { connect } from "react-redux";
import { ToastProvider } from 'react-toast-notifications';
import '../node_modules/bootstrap/dist/css/bootstrap.min.css';
function mapStateToProps(state) {
@ -28,17 +26,16 @@ function mapDispatchToProps(dispatch) {
const App = () => {
let exact = true
return (
<HashRouter>
<ToastProvider autoDismiss="true" autoDismissTimeout="5000" placement="bottom-right">
<Navigation />
<Switch>
<Route exact={exact} path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/login" component={Login} />
<Route path="/register" component={Register} />
</Switch>
</ToastProvider>
</HashRouter>
<div>
<Navigation />
<Switch>
<Route exact={exact} path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/settings" component={Settings} />
<Route path="/login" component={Login} />
<Route path="/register" component={Register} />
</Switch>
</div>
);
}

View File

@ -0,0 +1 @@
// https://stackoverflow.com/questions/38397653/redux-what-is-the-correct-place-to-save-cookie-after-login-request

View File

@ -0,0 +1,45 @@
import React, { Component, PropTypes } from 'react';
import { Provider } from 'react-redux';
import { persistStore } from 'redux-persist';
class AppProvider extends Component {
static propTypes = {
store: PropTypes.object.isRequired,
children: PropTypes.node
}
constructor(props) {
super(props);
this.state = { rehydrated: false };
}
componentWillMount() {
const opts = {
whitelist: ['user'] // <-- Your auth/user reducer storing the cookie
};
persistStore(this.props.store, opts, () => {
this.setState({ rehydrated: true });
});
}
render() {
if (!this.state.rehydrated) {
return null;
}
return (
<Provider store={this.props.store}>
{this.props.children}
</Provider>
);
}
}
AppProvider.propTypes = {
store: PropTypes.object.isRequired,
children: PropTypes.node
}
export default AppProvider;

View File

@ -2,7 +2,7 @@ import React from 'react';
import '../../App.css';
import './Login.css';
import { Button } from 'reactstrap';
import { Formik, Form, Field } from 'formik';
import { useToasts } from 'react-toast-notifications';
function withToast(Component) {
@ -29,16 +29,15 @@ class Login extends React.Component {
this.setState({password: event.target.value});
}
handleSubmit(event) {
event.preventDefault();
handleSubmit(values) {
this.setState({loading: true});
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
timeout: 5000,
body: JSON.stringify({
username: this.state.username,
password: this.state.password,
username: values.username,
password: values.password,
})
};
const apiUrl = process.env.REACT_APP_API_URL + '/api/v1/login';
@ -47,7 +46,7 @@ class Login extends React.Component {
.then((function(data) {
if (data.error) {
this.props.addToast(data.error, { appearance: 'error' });
} else {
} else if (data.token) {
this.props.addToast(data.token, { appearance: 'success' });
}
this.setState({loading: false});
@ -59,30 +58,35 @@ class Login extends React.Component {
}
render() {
let trueBool = true;
return (
<div className="pageWrapper">
<h1>
Login
</h1>
<div className="loginBody">
<form onSubmit={this.handleSubmit}>
<Formik
initialValues={{ username: '', password: '' }}
onSubmit={async values => this.handleSubmit(values)}
>
<Form>
<label>
Email / Username<br/>
<input
<Field
name="username"
type="text"
required={trueBool}
className="loginFields"
value={this.state.username}
onChange={this.handleUsernameChange}
/>
</label>
<br/>
<label>
Password<br/>
<input
<Field
name="password"
type="password"
required={trueBool}
className="loginFields"
value={this.state.password}
onChange={this.handlePasswordChange}
/>
</label>
<br/><br/>
@ -92,7 +96,8 @@ class Login extends React.Component {
className="loginButton"
disabled={this.state.loading}
>Login</Button>
</form>
</Form>
</Formik>
</div>
</div>
);

View File

@ -2,7 +2,6 @@ import React from 'react';
import '../../App.css';
import './Login.css';
import { Button } from 'reactstrap';
import { useToasts } from 'react-toast-notifications';
function withToast(Component) {
@ -84,64 +83,73 @@ class Register extends React.Component {
}
render() {
let trueBool = true;
return (
<div className="pageWrapper">
<h1>
Register
</h1>
<div className="loginBody">
<form onSubmit={this.handleSubmit}>
<label>
Username*<br/>
<input
type="text"
required="true"
className="loginFields"
value={this.state.username}
onChange={this.handleUsernameChange}
/>
</label>
<br/>
<label>
Email<br/>
<input
type="email"
className="loginFields"
value={this.state.email}
onChange={this.handleEmailChange}
/>
</label>
<br/>
<label>
Password<br/>
<input
type="password"
required="true"
className="loginFields"
value={this.state.password}
onChange={this.handlePasswordChange}
/>
</label>
<br/>
<label>
Password<br/>
<input
type="password"
required="true"
className="loginFields"
value={this.state.passwordconfirm}
onChange={this.handlePasswordConfirmChange}
/>
</label>
<br/><br/>
<Button
color="primary"
type="submit"
className="loginButton"
disabled={this.state.loading}
>Login</Button>
</form>
</div>
{
// TODO: Move to DB:config REGISTRATION_DISABLED=1|0
process.env.REACT_APP_REGISTRATION_DISABLED === "true" ?
<p>Registration is temporarily disabled. Please try again soon!</p>
:
<div>
<h1>
Register
</h1>
<div className="loginBody">
<form onSubmit={this.handleSubmit}>
<label>
Username*<br/>
<input
type="text"
required={trueBool}
className="loginFields"
value={this.state.username}
onChange={this.handleUsernameChange}
/>
</label>
<br/>
<label>
Email<br/>
<input
type="email"
className="loginFields"
value={this.state.email}
onChange={this.handleEmailChange}
/>
</label>
<br/>
<label>
Password<br/>
<input
type="password"
required={trueBool}
className="loginFields"
value={this.state.password}
onChange={this.handlePasswordChange}
/>
</label>
<br/>
<label>
Password<br/>
<input
type="password"
required={trueBool}
className="loginFields"
value={this.state.passwordconfirm}
onChange={this.handlePasswordConfirmChange}
/>
</label>
<br/><br/>
<Button
color="primary"
type="submit"
className="loginButton"
disabled={this.state.loading}
>Login</Button>
</form>
</div>
</div>
}
</div>
);
}

View File

View File

@ -0,0 +1,36 @@
import React from 'react';
import '../../App.css';
import './Settings.css';
import { useToasts } from 'react-toast-notifications';
function withToast(Component) {
return function WrappedComponent(props) {
const toastFuncs = useToasts()
return <Component {...props} {...toastFuncs} />;
}
}
class Settings extends React.Component {
constructor(props) {
super(props);
this.state = {username: '', password: '', loading: false};
}
render() {
return (
<div className="pageWrapper">
<h1>
Settings
</h1>
<div className="loginBody">
<p>
All the settings
</p>
</div>
</div>
);
}
}
export default withToast(Settings);

View File

@ -2,7 +2,8 @@ import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom'
import { HashRouter } from 'react-router-dom'
import { ToastProvider } from 'react-toast-notifications';
import { Provider } from 'react-redux'
import { createStore } from 'redux'
@ -14,10 +15,12 @@ const goScorbbleStore = (state = false, logIn) => {
const store = createStore(goScorbbleStore);
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
<HashRouter>
<ToastProvider autoDismiss="true" autoDismissTimeout="5000" placement="bottom-right">
<Provider store={store}>
<App />
</Provider>
</ToastProvider>
</HashRouter>,
document.getElementById('root')
);