Merge branch 'dev' of vincent/chainetv_web into master

This commit is contained in:
vincent 2019-05-08 18:14:40 +02:00 committed by Gitea
commit 75699aa733
13 changed files with 295 additions and 41 deletions

View File

@ -1,8 +1,44 @@
from flask import Blueprint, jsonify, request,make_response,redirect,url_for,render_template from flask import Blueprint, jsonify, request,make_response,redirect,url_for,render_template,current_app
from chainetv.Jsonfile import JSONfile from .Jsonfile import JSONfile
from chainetv import emission from . import emission
import jwt
from functools import wraps
from datetime import datetime, timedelta
from .user import User
data= JSONfile("chaine.json") data= JSONfile("chaine.json")
def token_required(f):
@wraps(f)
def _verify(*args, **kwargs):
auth_headers = request.headers.get('Authorization', '').split()
invalid_msg = {
'message': 'Invalid token. Registeration and / or authentication required',
'authenticated': False
}
expired_msg = {
'message': 'Expired token. Reauthentication required.',
'authenticated': False
}
if len(auth_headers) != 2:
return jsonify(invalid_msg), 401
try:
token = auth_headers[1]
data = jwt.decode(token,current_app.config['SECRET_KEY'])
user = User
if not user:
raise RuntimeError('User not found')
return f(user, *args, **kwargs)
except jwt.ExpiredSignatureError:
return jsonify(expired_msg), 401 # 401 is Unauthorized HTTP status code
except (jwt.InvalidTokenError) as e:
print(e)
return jsonify(invalid_msg), 401
return _verify
api = Blueprint("api", __name__) api = Blueprint("api", __name__)
@api.route('/ping', methods=['GET']) @api.route('/ping', methods=['GET'])
@ -18,7 +54,8 @@ def get_chaine(num):
return jsonify(chaine) return jsonify(chaine)
@api.route('/chaine/', methods=['put']) @api.route('/chaine/', methods=['put'])
def update_list(): @token_required
def update_list(user):
status=data.parsechaine() status=data.parsechaine()
if(status=='ok'): if(status=='ok'):
return jsonify("OK") return jsonify("OK")
@ -32,3 +69,28 @@ def get_emmission(num):
return make_response("",204) return make_response("",204)
else: else:
return jsonify(emission.parse_emmission(chaine)) return jsonify(emission.parse_emmission(chaine))
#@api.route('/register/', methods=('POST',))
#def register():
# data = request.get_json()
# user = User(**data)
# db.session.add(user)
# db.session.commit()
# return jsonify(user.to_dict()), 201
@api.route('/login/', methods=('POST',))
def login():
data = request.get_json()
user = User.authenticate(**data)
if not user:
return jsonify({ 'message': 'Invalid credentials', 'authenticated': False }), 401
token = jwt.encode({
'sub': user.name,
'iat':datetime.utcnow(),
'exp': datetime.utcnow() + timedelta(minutes=30)},
current_app.config['SECRET_KEY'])
return jsonify({ 'token': token.decode('UTF-8') })

View File

@ -7,7 +7,7 @@ def create_app(app_name=__name__):
app.config.from_object('chainetv.config.BaseConfig') app.config.from_object('chainetv.config.BaseConfig')
#app.wsgi_app = ReverseProxied(app.wsgi_app) #app.wsgi_app = ReverseProxied(app.wsgi_app)
CORS(app) CORS(app)
from chainetv.api import api from .api import api
app.register_blueprint(api, url_prefix="/api/v1") app.register_blueprint(api, url_prefix="/api/v1")
@app.route('/') @app.route('/')
@app.route('/<path:path>') @app.route('/<path:path>')

View File

@ -1,3 +1,3 @@
class BaseConfig(object): class BaseConfig(object):
DEBUG = True DEBUG = True
SECRET_KEY = 'mysecretkeyg' SECRET_KEY = '53FEFEGEGEGFTGETRF354353'

28
backend/chainetv/user.py Normal file
View File

@ -0,0 +1,28 @@
from werkzeug.security import generate_password_hash, check_password_hash
class User(object):
name="vincent"
password=generate_password_hash("test", method='sha256')
id=1
# def __init__(self, name, password):
# self.name = name
# self.password = generate_password_hash(password, method='sha256')
@classmethod
def authenticate(cls, **kwargs):
name = kwargs.get('name')
password = kwargs.get('password')
if not name or not password:
return None
user = cls
if not user or not check_password_hash(user.password, password):
return None
return user
def to_dict(self):
return dict(id=self.id, name=self.name)

View File

@ -4,12 +4,14 @@ bs4==0.0.1
Click==7.0 Click==7.0
Flask==1.0.2 Flask==1.0.2
Flask-Cors==3.0.7 Flask-Cors==3.0.7
gunicorn==19.9.0
isort==4.3.17 isort==4.3.17
itsdangerous==1.1.0 itsdangerous==1.1.0
Jinja2==2.10.1 Jinja2==2.10.1
lazy-object-proxy==1.3.1 lazy-object-proxy==1.3.1
MarkupSafe==1.1.1 MarkupSafe==1.1.1
mccabe==0.6.1 mccabe==0.6.1
PyJWT==1.7.1
pylint==2.3.1 pylint==2.3.1
six==1.12.0 six==1.12.0
soupsieve==1.9.1 soupsieve==1.9.1

View File

@ -18,4 +18,22 @@ export default {
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
h1,
h2 {
font-weight: normal;
font-size: 2em;
font-weight: bold;
}
p {
text-align: justify;
}
.is-success {
background-color: #4caf50;
}
.is-hoverable {
color: #42b983;
}
</style> </style>

View File

@ -13,8 +13,16 @@ export function fetchemission(num){
} }
export function putparsechaine(){ export function putparsechaine(jwt){
return axios.put(`${API_PATH}/chaine/`); return axios.put(`${API_PATH}/chaine/`,'',{ headers: { Authorization: `Bearer: ${jwt}` }});
} }
export function authenticate (userData) {
return axios.post(`${API_PATH}/login/`, userData)
}
// export function register (userData) {
// return axios.post(`${API_URL}/register/`, userData)
// }

View File

@ -100,39 +100,33 @@ export default {
}); });
}, },
parsechaine() { parsechaine() {
if (!this.$store.getters.isAuthenticated) {
return putparsechaine() this.$router.push('/login')
} else {
console.log(this.$store.state.jwt.token)
return putparsechaine(this.$store.state.jwt.token)
.then(res => { .then(res => {
if (res.data == "OK") { if (res.data == "OK") {
alert("update database OK"); alert("update database OK");
} }
}) })
.catch(res => { .catch(error => {
// eslint-disable-next-line // eslint-disable-next-line
alert("error during database update"); if (error.response) {
console.error(res); alert(error.response.data.message);
}else{
console.error(error);
}
this.$router.push('/login')
}); });
} }
}
} }
}; };
</script> </script>
<style scoped> <style >
h1,
h2 {
font-weight: normal;
font-size: 2em;
font-weight: bold;
}
p {
text-align: justify;
}
.is-success {
background-color: #4caf50;
}
.is-hoverable {
color: #42b983;
}
</style> </style>

View File

@ -0,0 +1,76 @@
<!-- components/Login.vue -->
<template>
<div>
<section class="hero is-success">
<div class="hero-body">
<div class="container has-text-centered">
<h2 class="title">Login </h2>
<p class="subtitle error-msg">{{ errorMsg }}</p>
</div>
</div>
</section>
<section class="section">
<div class="container">
<div class="field">
<label class="label is-large" for="name">Name:</label>
<div class="control">
<input type="name" class="input is-large" id="email" v-model="name">
</div>
</div>
<div class="field">
<label class="label is-large" for="password">Password:</label>
<div class="control">
<input type="password" class="input is-large" id="password" v-model="password">
</div>
</div>
<div class="control">
<a class="button is-large is-success" @click="authenticate">Login</a>
</div>
</div>
</section>
</div>
</template>
<script>
import {EventBus} from "../utils"
export default {
data () {
return {
name: '',
password: '',
errorMsg: ''
}
},
methods: {
authenticate () {
this.$store.dispatch('login', { name: this.name, password: this.password })
.then(() =>{
if (this.$store.getters.isAuthenticated){
this.$router.push('/')
}
}
)
},
register () {
this.$store.dispatch('register', { name: this.name, password: this.password })
.then(() => this.$router.push('/'))
}
},
mounted () {
EventBus.$on('failedRegistering', (msg) => {
this.errorMsg = msg
})
EventBus.$on('failedAuthentication', (msg) => {
this.errorMsg = msg
})
},
beforeDestroy () {
EventBus.$off('failedRegistering')
EventBus.$off('failedAuthentication')
}
}
</script>

View File

@ -2,7 +2,8 @@ import Vue from 'vue';
import Router from 'vue-router'; import Router from 'vue-router';
import ping from '@/components/ping'; import ping from '@/components/ping';
import chainetv from '@/components/chainetv'; import chainetv from '@/components/chainetv';
import login from '@/components/login';
import store from '@/store'
Vue.use(Router); Vue.use(Router);
export default new Router({ export default new Router({
@ -12,10 +13,22 @@ export default new Router({
name: 'chaineTV', name: 'chaineTV',
component: chainetv, component: chainetv,
}, },
{
path: '/login',
name: 'Login',
component: login
},
{ {
path: '/ping', path: '/ping',
name: 'ping', name: 'ping',
component: ping, component: ping,
beforeEnter (to, from, next) {
if (!store.getters.isAuthenticated) {
next('/login')
} else {
next()
}
}
}, },
], ],
}); });

View File

@ -2,11 +2,14 @@
import Vue from 'vue' import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import { fetchchaine, fetchemission } from '../api'; import { fetchchaine, fetchemission,authenticate } from '../api';
import { isValidJwt, EventBus } from '../utils'
Vue.use(Vuex) Vue.use(Vuex)
const state = { const state = {
// single source of data // single source of data
arrayresultchaines: [], arrayresultchaines: [],
user: {},
jwt: ''
} }
const API_PATH=`${process.env.ROOT_API}/api/v1`; const API_PATH=`${process.env.ROOT_API}/api/v1`;
const actions = { const actions = {
@ -32,9 +35,31 @@ const actions = {
context.commit('push_chaine',{chaine:chaine,name:name,emission:res_emission.data}) context.commit('push_chaine',{chaine:chaine,name:name,emission:res_emission.data})
}) })
.catch(error => { .catch(error => {
context.commit('push_chaine',{chaine:chaine,name:name})
console.error(error); console.error(error);
}); });
}, },
login (context, userData) {
context.commit('setUserData', { userData })
return authenticate(userData)
.then(response => context.commit('setJwtToken', { jwt: response.data }))
.catch(error => {
console.log('Error Authenticating: ', error)
EventBus.$emit('failedAuthentication', error.response.data.message)
})
},
// register (context, userData) {
// context.commit('setUserData', { userData })
// return register(userData)
// .then(context.dispatch('login', userData))
// .catch(error => {
// console.log('Error Registering: ', error)
// EventBus.$emit('failedRegistering: ', error)
// })
// }
} }
@ -46,11 +71,23 @@ const mutations = {
}else{ }else{
state.arrayresultchaines.push({chaine: payload.chaine,name: payload.name,emission: payload.emission}); state.arrayresultchaines.push({chaine: payload.chaine,name: payload.name,emission: payload.emission});
} }
},
setUserData (state, payload) {
console.log('setUserData payload = ', payload)
state.userData = payload.userData
},
setJwtToken (state, payload) {
console.log('setJwtToken payload = ', payload)
localStorage.token = payload.jwt.token
state.jwt = payload.jwt
} }
} }
const getters = { const getters = {
// reusable data accessors // reusable data accessors
isAuthenticated (state) {
return isValidJwt(state.jwt.token)
}
} }
const store = new Vuex.Store({ const store = new Vuex.Store({

13
client/src/utils/index.js Normal file
View File

@ -0,0 +1,13 @@
import Vue from 'vue'
export const EventBus = new Vue()
export function isValidJwt (jwt) {
if (!jwt || jwt.split('.').length < 3) {
return false
}
const data = JSON.parse(atob(jwt.split('.')[1]))
const exp = new Date(data.exp * 1000) // JS deals with dates in milliseconds since epoch
const now = new Date()
return now < exp
}

3
readme.md Normal file
View File

@ -0,0 +1,3 @@
#chaine_tv_web
- test request authent:`curl -X POST -H 'Content-Type: application/json' -i 'http://127.0.0.1:5000/api/v1/login/' --data '{"email":"vincent","password":"test"}'`