diff --git a/.env.example b/.env.example index 911a1e45..64c5ad79 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,11 @@ MYSQL_USER= MYSQL_PASS= MYSQL_DB= +REDIS_URL= +REDIS_DB= +REDIS_PREFIX="gs:" +REDIS_AUTH="" + JWT_SECRET= JWT_EXPIRY=86400 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f1fdafea..c43110a3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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: diff --git a/README.md b/README.md index fad570b6..4164e014 100644 --- a/README.md +++ b/README.md @@ -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; @@ -31,4 +31,8 @@ We need to build NPM package, and then ship web/build with the binary. cp .env.example .env # Fill in the blanks cd web npm install --production && REACT_APP_API_URL=https://goscrobble.com npm run build go build -o goscrobble cmd/go-scrobble/*.go - ./goscrobble \ No newline at end of file + ./goscrobble + + +## Support Development! +Feel free to support hosting and my coffee addiction https://liberapay.com/idanoo \ No newline at end of file diff --git a/docs/config.md b/docs/config.md new file mode 100644 index 00000000..13ff35b0 --- /dev/null +++ b/docs/config.md @@ -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 \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json index 673548f3..664fceb3 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -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", diff --git a/web/package.json b/web/package.json index d5608955..e6c794f8 100644 --- a/web/package.json +++ b/web/package.json @@ -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": { diff --git a/web/src/App.js b/web/src/App.js index 26d9422f..30b0c7a2 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -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 ( - - - - - - - - - - - +
+ + + + + + + + +
); } diff --git a/web/src/Components/Api.js b/web/src/Components/Api.js new file mode 100644 index 00000000..96aca770 --- /dev/null +++ b/web/src/Components/Api.js @@ -0,0 +1 @@ +// https://stackoverflow.com/questions/38397653/redux-what-is-the-correct-place-to-save-cookie-after-login-request \ No newline at end of file diff --git a/web/src/Components/AppProvider.js b/web/src/Components/AppProvider.js new file mode 100644 index 00000000..aac876a1 --- /dev/null +++ b/web/src/Components/AppProvider.js @@ -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 ( + + {this.props.children} + + ); + } +} + +AppProvider.propTypes = { + store: PropTypes.object.isRequired, + children: PropTypes.node +} + +export default AppProvider; \ No newline at end of file diff --git a/web/src/Components/Pages/Login.js b/web/src/Components/Pages/Login.js index 38fde58d..7a3521d7 100644 --- a/web/src/Components/Pages/Login.js +++ b/web/src/Components/Pages/Login.js @@ -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 (

Login

-
+ this.handleSubmit(values)} + > +


@@ -92,7 +96,8 @@ class Login extends React.Component { className="loginButton" disabled={this.state.loading} >Login - + +
); diff --git a/web/src/Components/Pages/Register.js b/web/src/Components/Pages/Register.js index bbd60aa0..de8abeed 100644 --- a/web/src/Components/Pages/Register.js +++ b/web/src/Components/Pages/Register.js @@ -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 (
-

- Register -

-
-
- -
- -
- -
- -

- -
-
+ { + // TODO: Move to DB:config REGISTRATION_DISABLED=1|0 + process.env.REACT_APP_REGISTRATION_DISABLED === "true" ? +

Registration is temporarily disabled. Please try again soon!

+ : +
+

+ Register +

+
+
+ +
+ +
+ +
+ +

+ +
+
+
+ }
); } diff --git a/web/src/Components/Pages/Settings.css b/web/src/Components/Pages/Settings.css new file mode 100644 index 00000000..e69de29b diff --git a/web/src/Components/Pages/Settings.js b/web/src/Components/Pages/Settings.js new file mode 100644 index 00000000..6b3b9f72 --- /dev/null +++ b/web/src/Components/Pages/Settings.js @@ -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 ; + } +} + +class Settings extends React.Component { + constructor(props) { + super(props); + this.state = {username: '', password: '', loading: false}; + } + + render() { + return ( +
+

+ Settings +

+
+

+ All the settings +

+
+
+ ); + } +} + +export default withToast(Settings); diff --git a/web/src/index.js b/web/src/index.js index c0bdb1b6..37507d55 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -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( - - - - - , + + + + + + + , document.getElementById('root') );