added change password, style updates, api updates

This commit is contained in:
aj 2019-07-30 16:25:01 +01:00
parent 14a3c0bab1
commit 6617160c75
16 changed files with 510 additions and 139 deletions

View File

@ -12,6 +12,7 @@
# below: # below:
.git .git
.gitignore .gitignore
.vscode
venv venv
node_modules node_modules

1
.gitignore vendored
View File

@ -7,6 +7,7 @@ node_modules/
*$py.class *$py.class
.idea .idea
.vscode
# C extensions # C extensions
*.so *.so

View File

@ -1,5 +1,6 @@
from flask import Blueprint, session, request, jsonify from flask import Blueprint, session, request, jsonify
from google.cloud import firestore from google.cloud import firestore
from werkzeug.security import check_password_hash, generate_password_hash
blueprint = Blueprint('api', __name__) blueprint = Blueprint('api', __name__)
db = firestore.Client() db = firestore.Client()
@ -20,18 +21,65 @@ def get_playlists():
error = {'error': 'multiple usernames?'} error = {'error': 'multiple usernames?'}
return jsonify(error), 500 return jsonify(error), 500
docs = playlists.stream()
response = { response = {
'playlists': [i.to_dict() for i in docs] 'playlists': [i.to_dict() for i in playlists.stream()]
} }
return jsonify(response) return jsonify(response), 200
else: else:
error = {'error': 'username not in session, not logged in?'} error = {'error': 'not logged in'}
return jsonify(error), 401
@blueprint.route('/playlist', methods=['GET', 'POST', 'PUT'])
def get_playlist():
if 'username' in session:
users = db.collection(u'spotify_users').where(u'username', u'==', session['username']).stream()
users = [i for i in users]
if len(users) == 1:
playlists = db.document(u'spotify_users/{}'.format(users[0].id)).collection(u'playlists')
else:
error = {'error': 'multiple usernames?'}
return jsonify(error), 500 return jsonify(error), 500
if request.method == 'GET':
playlist_name = request.args.get('name', None)
if playlist_name:
playlist = [i for i in playlists.where(u'name', u'==', playlist_name).stream()]
if len(playlist) == 0:
return jsonify({'error': 'no playlist found'}), 404
elif len(playlist) > 1:
return jsonify({'error': 'multiple playlists found'}), 500
return jsonify(playlist[0].to_dict()), 200
else:
response = {"error": 'no name requested'}
return jsonify(response), 400
elif request.method == 'POST' or request.method == 'PUT':
request_json = request.get_json()
if 'name' not in request_json:
return jsonify({'error': "no name provided"}), 400
playlist_name = request_json['name']
return 404
else:
error = {'error': 'not logged in'}
return jsonify(error), 401
@blueprint.route('/user', methods=['GET']) @blueprint.route('/user', methods=['GET'])
def user(): def user():
@ -55,13 +103,59 @@ def user():
'validated': doc['validated'] 'validated': doc['validated']
} }
return jsonify(response) return jsonify(response), 200
else: else:
error = {'error': 'username not in session, not logged in?'} error = {'error': 'not logged in'}
return jsonify(error), 404 return jsonify(error), 401
@blueprint.route('/user/password', methods=['POST'])
def change_password():
request_json = request.get_json()
if 'username' in session:
if 'new_password' in request_json and 'current_password' in request_json:
if len(request_json['new_password']) == 0:
response = {"error": 'zero length password'}
return jsonify(response), 400
if len(request_json['new_password']) > 30:
response = {"error": 'password too long'}
return jsonify(response), 400
users = db.collection(u'spotify_users').where(u'username', u'==', session['username']).stream()
users = [i for i in users]
if len(users) == 1:
current_user = db.collection(u'spotify_users').document(u'{}'.format(users[0].id))
else:
error = {'error': 'multiple usernames?'}
return jsonify(error), 500
if check_password_hash(current_user.get().to_dict()['password'], request_json['current_password']):
current_user.update({'password': generate_password_hash(request_json['new_password'])})
response = {"message": 'password changed', "status": "success"}
return jsonify(response), 200
else:
error = {'error': 'wrong password provided'}
return jsonify(error), 403
else:
error = {'error': 'malformed request, no old_password/new_password'}
return jsonify(error), 400
else:
error = {'error': 'not logged in'}
return jsonify(error), 401
@blueprint.route('/playlist', methods=['GET', 'PUT', 'POST']) @blueprint.route('/playlist', methods=['GET', 'PUT', 'POST'])
def playlist(): def playlist():
return 404 return 401

View File

@ -21,7 +21,11 @@ def login():
users = [i for i in users] users = [i for i in users]
if len(users) != 1: if len(users) == 0:
flash('user not found')
return redirect(url_for('index'))
if len(users) > 1:
flash('multiple users found') flash('multiple users found')
return redirect(url_for('index')) return redirect(url_for('index'))

View File

@ -69,9 +69,9 @@ def token():
return redirect('/app') return redirect('/app')
@app.route('/app') @app.route('/app', defaults={'path': ''})
@app.route('/app/<path>') @app.route('/app/<path:path>')
def app_route(path = None): def app_route(path):
if 'username' not in session: if 'username' not in session:
flash('please log in') flash('please log in')

13
src/js/Admin/Admin.js Normal file
View File

@ -0,0 +1,13 @@
import React, { Component } from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
const axios = require('axios');
class Admin extends Component {
render(){
return (
<p>admin</p>
);
}
}
export default Admin

7
src/js/Error/NotFound.js Normal file
View File

@ -0,0 +1,7 @@
import React from "react";
function NotFound (props) {
return <p style={{textAlign: "center"}}>404: Path Not Found</p>;
}
export default NotFound;

View File

@ -5,10 +5,8 @@ class Index extends Component{
constructor(props){ constructor(props){
super(props); super(props);
this.state = { this.state = {}
// this.pingPlaylists();
}
this.pingPlaylists();
} }
pingPlaylists(){ pingPlaylists(){

View File

@ -6,8 +6,9 @@ class PlaylistView extends Component{
constructor(props){ constructor(props){
super(props); super(props);
console.log(this.props);
this.state = { this.state = {
name: props.name name: this.props.match.name
} }
} }

View File

@ -2,58 +2,21 @@ import React, { Component } from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom"; import { BrowserRouter as Router, Route, Link } from "react-router-dom";
const axios = require('axios'); const axios = require('axios');
import PlaylistsView from "./PlaylistsView.js"
class Playlists extends Component { class Playlists extends Component {
constructor(props){
super(props);
this.state = {
isLoading: true
}
this.getPlaylists();
}
getPlaylists(){
var self = this;
axios.get('/api/playlists')
.then((response) => {
self.setState({
playlists: response.data.playlists,
isLoading: false
});
});
}
render(){ render(){
const table = <div><Table playlists={this.state.playlists}/></div>;
const loadingMessage = <p className="center-text">loading...</p>;
return this.state.isLoading ? loadingMessage : table;
}
}
function Table(props){
return ( return (
<div> <div>
{ props.playlists.map((playlist) => <Row playlist={ playlist } key={ playlist.name }/>) } <ul className="navbar" style={{width: "100%"}}>
<li><Link to={`${this.props.match.url}/add`}>add</Link></li>
</ul>
<Route path={`${this.props.match.url}/`} component={PlaylistsView} />
</div> </div>
); );
} }
function Row(props){
return (
<PlaylistLink playlist={props.playlist}/>
);
}
function PlaylistLink(props){
return (
<Link to={ getPlaylistLink(props.playlist.name) }>{ props.playlist.name }</Link>
);
}
function getPlaylistLink(playlistName){
return '/app/playlist/' + playlistName;
} }
export default Playlists; export default Playlists;

View File

@ -0,0 +1,65 @@
import React, { Component } from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
const axios = require('axios');
class PlaylistsView extends Component {
constructor(props){
super(props);
this.state = {
isLoading: true
}
this.getPlaylists();
}
getPlaylists(){
var self = this;
axios.get('/api/playlists')
.then((response) => {
self.setState({
playlists: response.data.playlists,
isLoading: false
});
});
}
render() {
const table = <div><Table playlists={this.state.playlists}/></div>;
const loadingMessage = <p className="center-text">loading...</p>;
return this.state.isLoading ? loadingMessage : table;
}
}
function Table(props){
return (
<table className="app-table max-width">
<tbody>
{ props.playlists.map((playlist) => <Row playlist={ playlist } key={ playlist.name }/>) }
</tbody>
</table>
);
}
function Row(props){
return (
<tr>
<PlaylistLink playlist={props.playlist}/>
</tr>
);
}
function PlaylistLink(props){
return (
<td>
<Link to={ getPlaylistLink(props.playlist.name) } className="button full-width">{ props.playlist.name }</Link>
</td>
);
}
function getPlaylistLink(playlistName){
return '/app/playlist/' + playlistName;
}
export default PlaylistsView;

View File

@ -1,29 +1,71 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom"; import { BrowserRouter as Router, Route, Link, Switch, Redirect} from "react-router-dom";
import Index from "./Index.js"; import Index from "./Index/Index.js";
import Playlists from "./Playlist/Playlists.js"; import Playlists from "./Playlist/Playlists.js";
import PlaylistView from "./Playlist/PlaylistView.js"; import PlaylistView from "./Playlist/PlaylistView.js";
import Settings from "./Settings.js"; import Settings from "./Settings/Settings.js";
import Admin from "./Admin/Admin.js";
import NotFound from "./Error/NotFound.js";
const axios = require('axios');
class PlaylistManager extends Component { class PlaylistManager extends Component {
constructor(props){
super(props);
this.state = {
type: null
}
}
componentDidMount() {
this.getUserInfo();
}
componentWillUnmount() {
this.userInfoCancelToken.cancel();
}
getUserInfo(){
this.userInfoCancelToken = axios.CancelToken.source();
var self = this;
axios.get('/api/user', {
cancelToken: this.userInfoCancelToken.token
})
.then((response) => {
self.setState({
type: response.data.type
})
});
}
render(){ render(){
return ( return (
<Router> <Router>
<div className="card pad-12"> <div className="card pad-12">
<ul className="sidebar pad-3"> <table className="sidebar pad-3">
<li><Link to="/app">home</Link></li> <tbody>
<li><Link to="/app/playlists">playlists</Link></li> <tr><td><span><Link to="/app">home</Link></span></td></tr>
<li><Link to="/app/settings">settings</Link></li> <tr><td><Link to="/app/playlists">playlists</Link></td></tr>
<li><a href="/auth/logout">logout</a></li> <tr><td><Link to="/app/settings">settings</Link></td></tr>
<li><a href="https://sarsoo.xyz">sarsoo.xyz</a></li> { this.state.type == 'admin' && <tr><td><Link to="/app/admin">admin</Link></td></tr> }
</ul> <tr><td><a href="/auth/logout">logout</a></td></tr>
<tr><td><a href="https://sarsoo.xyz">sarsoo.xyz</a></td></tr>
</tbody>
</table>
<div className="pad-9"> <div className="pad-9">
<Switch>
<Route path="/app" exact component={Index} /> <Route path="/app" exact component={Index} />
<Route path="/app/playlists" exact component={Playlists} /> <Route path="/app/playlists" exact component={Playlists} />
<Route path="/app/settings" component={Settings} /> <Route path="/app/settings" component={Settings} />
{ this.state.type == 'admin' && <Route path="/app/admin" component={Admin} /> }
<Route path='/app/playlist/:name' component={PlaylistView} />
<Route component={NotFound} />
</Switch>
</div> </div>
</div> </div>

View File

@ -1,8 +0,0 @@
import React, { Component } from "react";
function Settings(props){
return <p>settings</p>;
}
export default Settings;

View File

@ -0,0 +1,126 @@
import React, { Component } from "react";
const axios = require('axios');
class ChangePassword extends Component {
constructor(props){
super(props);
this.state = {
current: "",
new1: "",
new2: "",
error: false,
errorValue: null
}
this.handleCurrentChange = this.handleCurrentChange.bind(this);
this.handleNewChange = this.handleNewChange.bind(this);
this.handleNew2Change = this.handleNew2Change.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleCurrentChange(event){
this.setState({
'current': event.target.value
});
}
handleNewChange(event){
this.setState({
'new1': event.target.value
});
}
handleNew2Change(event){
this.setState({
'new2': event.target.value
});
}
handleSubmit(event){
if(this.state.current.length == 0){
this.setState({
error: true,
errorValue: "enter current password"
});
}else{
if(this.state.new1.length == 0){
this.setState({
error: true,
errorValue: "enter new password"
});
}else{
if(this.state.new1 != this.state.new2){
this.setState({
error: true,
errorValue: "new password mismatch"
});
}else{
axios.post('/api/user/password',{
current_password: this.state.current,
new_password: this.state.new1
}).then((response) => {
this.setState({
error: true,
errorValue: "password changed"
});
}).catch((error) => {
this.setState({
error: true,
errorValue: "error changing password"
});
});
}
}
}
event.preventDefault();
}
render(){
return (
<div>
<h1>change password</h1>
<form onSubmit={this.handleSubmit}>
<table className="app-table max-width">
<tbody>
<tr>
<td className="ui-text center-text">current:</td>
<td><input
type="password"
name="current"
value={this.state.current}
onChange={this.handleCurrentChange}
className="full-width" /></td>
</tr>
<tr>
<td className="ui-text center-text">new:</td>
<td><input
type="password"
name="new1"
value={this.state.new1}
onChange={this.handleNewChange}
className="full-width" /></td>
</tr>
<tr>
<td className="ui-text center-text">new again:</td>
<td><input
type="password"
name="new2"
value={this.state.new2}
onChange={this.handleNew2Change}
className="full-width" /></td>
</tr>
<tr>
<td colSpan="2"><input type="submit" style={{width: "100%"}} className="button" value="change" /></td>
</tr>
</tbody>
</table>
</form>
{ this.state.error && <p style={{color: "red"}}>{this.state.errorValue}</p>}
</div>
);
}
}
export default ChangePassword;

View File

@ -0,0 +1,24 @@
import React, { Component } from "react";
import { BrowserRouter as Router, Route, Link, Switch, Redirect} from "react-router-dom";
import ChangePassword from "./ChangePassword.js"
class Settings extends Component {
render() {
return (
<div>
<ul className="navbar" style={{width: "100%"}}>
<li><Link to={`${this.props.match.url}/password`}>password</Link></li>
</ul>
<Route path={`${this.props.match.url}/password`} component={ChangePassword} />
</div>
);
}
}
export default Settings;

View File

@ -1,6 +1,7 @@
$font-stack: 'Lato', arial; $font-stack: 'Lato', arial;
$background-colour: #202124; $background-colour: #202124;
$ui-colour: #131313; $ui-colour: #131313;
$light-ui: #575757;
$text-colour: white; $text-colour: white;
$pad-px: 20px; $pad-px: 20px;
@ -30,6 +31,10 @@ p {
padding: 10px; padding: 10px;
} }
.ui-text {
color: $text-colour;
}
.center-text { .center-text {
text-align: center; text-align: center;
} }
@ -40,34 +45,17 @@ p {
.button { .button {
background-color: #505050; background-color: #505050;
color: white; color: $text-colour;
border-radius: 10px;
display: inline-block; display: inline-block;
margin: 4px auto;
cursor: pointer;
padding: 15px;
box-shadow: 2px 2px 4px black;
/*-webkit-transition-duration: 0.4s;
transition-duration: 0.4s;*/
text: {
align: center;
decoration: none;
}
&:hover {
box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24),0 17px 50px 0 rgba(0,0,0,0.19);
}
}
button {
background-color: #505050;
color: white;
border-radius: 10px; border-radius: 10px;
display: inline-block; border: none;
margin: 4px auto; margin: 4px auto;
cursor: pointer;
padding: 15px; padding: 15px;
cursor: pointer;
box-shadow: 2px 2px 4px black; box-shadow: 2px 2px 4px black;
/*-webkit-transition-duration: 0.4s; /*-webkit-transition-duration: 0.4s;
transition-duration: 0.4s;*/ transition-duration: 0.4s;*/
@ -128,6 +116,12 @@ h1.title {
font-family: 'Pirata One', arial; font-family: 'Pirata One', arial;
text-shadow: 3px 3px 3px #aaa; text-shadow: 3px 3px 3px #aaa;
cursor: default;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
font-weight: bold; font-weight: bold;
display: block; display: block;
text-decoration: none; text-decoration: none;
@ -152,7 +146,7 @@ h1.title {
h1 { h1 {
text-align: center; text-align: center;
color: white; color: $text-colour;
text-shadow: 1px 1px 2px #4f4f4f; text-shadow: 1px 1px 2px #4f4f4f;
} }
@ -172,7 +166,7 @@ h1.title {
h1.sectiontitle { h1.sectiontitle {
text-align:center; text-align:center;
color: white; color: $text-colour;
font-family: 'Megrim', sans-serif; font-family: 'Megrim', sans-serif;
} }
@ -183,8 +177,7 @@ ul.navbar {
margin: 10px; margin: 10px;
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
background-color: $ui-colour; background-color: $light-ui;
li { li {
float: left; float: left;
@ -195,9 +188,10 @@ ul.navbar {
a { a {
display: block; display: block;
color: white; color: $text-colour;
text-align: center; text-align: center;
padding: 14px 16px; padding: 14px 16px;
text-decoration: none; text-decoration: none;
text-shadow: 1px 1px 2px black; text-shadow: 1px 1px 2px black;
-webkit-transition: background-color 0.4s; -webkit-transition: background-color 0.4s;
@ -214,31 +208,77 @@ ul.navbar {
} }
.sidebar { .sidebar {
li {
// float: left;
position: -webkit-sticky;
position: sticky;
top: 0;
height: 50px;
background-color: $light-ui;
list-style-type: none;
margin-left: 10px;
margin-right: 10px;
border-radius: 5px;
padding-top: 10px;
padding-bottom: 10px;
tr {
// float: left;
// position: -webkit-sticky;
// position: sticky;
top: 0;
cursor: pointer;
// padding: 10px;
td {
height: 50px;
text-align: center;
vertical-align: center;
border-radius: 5px;
// margin-top: 10px;
// margin-bottom: 10px;
// margin: auto;
a { a {
// display: block; // display: block;
color: white; color: $text-colour;
text-align: center; // text-align: center;
padding: 14px 16px; // vertical-align: center;
// margin: 25px;
// padding: 14px 16px;
// border-radius: 5px;
height: 100%;
width: 100%;
display: inline-block;
vertical-align: center;
padding: 10px;
text-decoration: none; text-decoration: none;
text-shadow: 1px 1px 2px black; text-shadow: 1px 1px 2px black;
-webkit-transition: background-color 0.4s; -webkit-transition: background-color 0.4s;
transition: background-color 0.4s; transition: background-color 0.4s;
}
&:hover { &:hover {
background-color: #080808; background-color: black;
} }
} }
} }
}
.sidebar-selected {
background-color: black;
}
.app-table {
width: 100%;
margin: auto;
td {
padding: $pad-px;
}
}
.max-width {
max-width: 800px;
} }
footer { footer {
@ -257,7 +297,7 @@ footer {
} }
} }
@media only screen and (max-width: 600px) { @media only screen and (max-width: 768px) {
ul.navbar li.right, ul.navbar li.right,
ul.navbar li {float: none;} ul.navbar li {float: none;}
} }