diff --git a/README.md b/README.md
index 3a265574..a8f62c54 100644
--- a/README.md
+++ b/README.md
@@ -77,6 +77,20 @@ Run npm install in fact-bounty-client folder.
export ELASTIC_SEARCH_URL=""
export ELASTIC_SEARCH_USERNAME=""
export ELASTIC_SEARCH_PASSWORD=""
+
+ export TZ="Asia/Colombo"
+
+ export MAIL_USERNAME=""
+ export MAIL_PASSWORD=""
+ export FACTBOUNTY_ADMIN=""
+ export MAIL_PORT="587"
+ export MAIL_USE_TLS="true"
+ export MAIL_SERVER="smtp.gmail.com"
+
+ export ADMIN_USERNAME = "admin"
+ export ADMIN_PASSWORD = "password"
+ export ADMIN_EMAIL = "email@email.com"
+
export TZ=“Asia/Colombo”
```
@@ -107,12 +121,12 @@ Run npm install in fact-bounty-client folder.
### How to install Elasticsearch and start elasticsearch server
-* #### Elasticsearch v6.7.0 can be installed as follows:
+* #### Elasticsearch v7.6.0 can be installed as follows:
```
- (venv)$ wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.7.0.deb
- (venv)$ wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.7.0.deb.sha512
- (venv)$ shasum -a 512 -c elasticsearch-6.7.0.deb.sha512
- (venv)$ sudo dpkg -i elasticsearch-6.7.0.deb
+ (venv)$ wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.6.0.deb
+ (venv)$ wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.6.0.deb.sha512
+ (venv)$ shasum -a 512 -c elasticsearch-7.6.0.deb.sha512
+ (venv)$ sudo dpkg -i elasticsearch-7.6.0.deb
```
@@ -202,6 +216,10 @@ And use [localhost:3000](https://) to browse.
* #### Once build completes, run `docker-compose up`
+## Setting up an OAuth Daemon
+
+[How to setup OAuth Daemon](OAuthdSetup.md)
+
# How to Contribute
- First fork the repository and clone it.
@@ -211,3 +229,4 @@ And use [localhost:3000](https://) to browse.
## License
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fscorelab%2Ffact-Bounty.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fscorelab%2Ffact-Bounty?ref=badge_large)
+
diff --git a/fact-bounty-client/src/AppRouter.js b/fact-bounty-client/src/AppRouter.js
index dbbff8c5..f2fc5fab 100755
--- a/fact-bounty-client/src/AppRouter.js
+++ b/fact-bounty-client/src/AppRouter.js
@@ -13,7 +13,8 @@ import Dashboard from './pages/Dashboard'
import Posts from './pages/Posts'
import PostDetailView from './pages/PostDetailView'
import Tweets from './pages/Tweets'
-
+import ForgotPassword from './pages/ForgotPassword'
+import ResetPassword from './pages/ResetPassword'
class AppRouter extends Component {
render() {
return (
@@ -23,6 +24,12 @@ class AppRouter extends Component {
+
+
diff --git a/fact-bounty-client/src/pages/ForgotPassword/ForgotPassword.jsx b/fact-bounty-client/src/pages/ForgotPassword/ForgotPassword.jsx
new file mode 100644
index 00000000..52c89977
--- /dev/null
+++ b/fact-bounty-client/src/pages/ForgotPassword/ForgotPassword.jsx
@@ -0,0 +1,255 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { connect } from 'react-redux'
+import classnames from 'classnames'
+import compose from 'recompose/compose'
+import Avatar from '@material-ui/core/Avatar'
+import Button from '@material-ui/core/Button'
+import CssBaseline from '@material-ui/core/CssBaseline'
+import FormControl from '@material-ui/core/FormControl'
+import Input from '@material-ui/core/Input'
+import InputLabel from '@material-ui/core/InputLabel'
+import LockOutlinedIcon from '@material-ui/icons/LockOutlined'
+import Paper from '@material-ui/core/Paper'
+import Typography from '@material-ui/core/Typography'
+import withStyles from '@material-ui/core/styles/withStyles'
+import Toast from '../../components/Toast'
+import { updateError } from '../../redux/actions/errorActions'
+import { updateSuccess } from '../../redux/actions/successActions'
+import {
+ forgotPassword,
+ authVerificationToken
+} from '../../redux/actions/authActions'
+import styles from './ForgotPassword.style'
+
+class ForgotPassword extends Component {
+ constructor() {
+ super()
+ this.state = {
+ email: '',
+ verificationToken: '',
+ errors: {},
+ success: {},
+ emailValid: false,
+ verificationTokenValid: false,
+ formValid: false,
+ openToast: false
+ }
+ }
+
+ componentDidMount() {
+ // If logged in and user navigates to Register page, should redirect them to dashboard
+ this.props.updateError({})
+ this.props.updateSuccess({})
+ if (this.props.auth.isAuthenticated) {
+ this.props.history.push('/dashboard')
+ }
+ }
+
+ static getDerivedStateFromProps(props, state) {
+ if (props.errors) {
+ const errors = props.errors
+ let openToast = false
+ if (errors.fetch) {
+ openToast = true
+ }
+ return { errors, openToast }
+ }
+ if (props.success) {
+ const success = props.success
+ let openToast = false
+ if (success.fetch) {
+ openToast = true
+ }
+ return { success, openToast }
+ }
+ return null
+ }
+
+ onChange = e => {
+ let { id, value } = e.target
+ this.setState({ [id]: value }, () => {
+ this.validateField(id, value)
+ })
+ }
+
+ validateField = (fieldname, value) => {
+ let { emailValid, verificationTokenValid, errors } = this.state
+
+ emailValid = value.match(/^([\w.%+-]+)@([\w-]+\.)+([\w]{2,})$/i)
+ verificationTokenValid = !!this.state.verificationToken
+ switch (fieldname) {
+ case 'email':
+ errors.email = emailValid ? '' : 'Invalid E-mail'
+ break
+ default:
+ break
+ }
+ this.setState(
+ {
+ errors,
+ emailValid,
+ verificationTokenValid
+ },
+ this.validateForm
+ )
+ }
+
+ validateForm = () => {
+ this.setState({
+ formValid: this.state.emailValid || this.state.verificationToken !== ''
+ })
+ }
+ closeToast = () => {
+ // Remove error from store
+ this.props.updateError({})
+ this.props.updateSuccess({})
+ this.setState({ openToast: false })
+ }
+
+ onSubmit = e => {
+ e.preventDefault()
+ // Remove error from store
+ this.props.updateSuccess({})
+ this.props.updateError({})
+
+ const { email } = this.state
+ if (!this.props.success.message) {
+ this.props.forgotPassword({ email: email })
+ } else {
+ this.props.authVerificationToken(
+ {
+ verification_token: this.state.verificationToken
+ },
+ this.props.history
+ )
+ }
+ }
+
+ render() {
+ const { errors, openToast } = this.state
+ var formInput, buttonName
+ if (!this.props.success.message) {
+ formInput = (
+
+ Email Address
+
+
+ {errors.email}
+
+
+ )
+ buttonName = 'Send verification code'
+ } else {
+ formInput = (
+
+
+ Verification token
+
+
+
+ )
+ buttonName = 'Verify token'
+ }
+ return (
+
+
+
+ {errors.fetch ? (
+
+ ) : null}
+
+
+
+
+ Forgot Password
+
+
+
+
+
+ )
+ }
+}
+
+ForgotPassword.propTypes = {
+ forgotPassword: PropTypes.func.isRequired,
+ authVerificationToken: PropTypes.func.isRequired,
+ auth: PropTypes.object.isRequired,
+ errors: PropTypes.object.isRequired,
+ success: PropTypes.object.isRequired,
+ history: PropTypes.object,
+ classes: PropTypes.object,
+ updateError: PropTypes.func.isRequired,
+ updateSuccess: PropTypes.func.isRequired
+}
+
+const mapStateToProps = state => ({
+ auth: state.auth,
+ errors: state.errors,
+ success: state.success
+})
+
+export default compose(
+ withStyles(styles, {
+ name: 'ForgotPassword'
+ }),
+ connect(mapStateToProps, {
+ forgotPassword,
+ authVerificationToken,
+ updateError,
+ updateSuccess
+ })
+)(ForgotPassword)
diff --git a/fact-bounty-client/src/pages/ForgotPassword/ForgotPassword.style.js b/fact-bounty-client/src/pages/ForgotPassword/ForgotPassword.style.js
new file mode 100644
index 00000000..4af949a6
--- /dev/null
+++ b/fact-bounty-client/src/pages/ForgotPassword/ForgotPassword.style.js
@@ -0,0 +1,32 @@
+export default theme => ({
+ main: {
+ width: 'auto',
+ display: 'block', // Fix IE 11 issue.
+ marginLeft: theme.spacing.unit * 3,
+ marginRight: theme.spacing.unit * 3,
+ [theme.breakpoints.up(400 + theme.spacing.unit * 3 * 2)]: {
+ width: 400,
+ marginLeft: 'auto',
+ marginRight: 'auto'
+ }
+ },
+ paper: {
+ marginTop: theme.spacing.unit * 12,
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ padding: `${theme.spacing.unit * 2}px ${theme.spacing.unit * 3}px ${theme
+ .spacing.unit * 3}px`
+ },
+ avatar: {
+ margin: theme.spacing.unit,
+ backgroundColor: theme.palette.secondary.main
+ },
+ form: {
+ width: '100%', // Fix IE 11 issue.
+ marginTop: theme.spacing.unit
+ },
+ submit: {
+ marginTop: theme.spacing.unit * 3
+ }
+})
diff --git a/fact-bounty-client/src/pages/ForgotPassword/index.js b/fact-bounty-client/src/pages/ForgotPassword/index.js
new file mode 100644
index 00000000..7a92c0ce
--- /dev/null
+++ b/fact-bounty-client/src/pages/ForgotPassword/index.js
@@ -0,0 +1,2 @@
+import ForgotPassword from './ForgotPassword'
+export default ForgotPassword
diff --git a/fact-bounty-client/src/pages/Login/Login.jsx b/fact-bounty-client/src/pages/Login/Login.jsx
index b067d9e6..1efed1b4 100644
--- a/fact-bounty-client/src/pages/Login/Login.jsx
+++ b/fact-bounty-client/src/pages/Login/Login.jsx
@@ -228,7 +228,13 @@ class Login extends Component {
>
Login
-
+
{
+ let { id, value } = e.target
+ this.setState({ [id]: value }, () => {
+ this.validateField(id, value)
+ })
+ }
+
+ handleClickShowPassword = () => {
+ this.setState(state => ({ showPassword: !state.showPassword }))
+ }
+
+ handleClickShowPassword2 = () => {
+ this.setState(state => ({ showPassword2: !state.showPassword2 }))
+ }
+
+ validateField = (fieldName, value) => {
+ let { passwordValid, password2Valid, errors } = this.state
+
+ switch (fieldName) {
+ case 'password':
+ passwordValid = value.length >= 8
+ errors.password = passwordValid ? '' : 'Too short!'
+ password2Valid = value === this.state.password2
+ if (password2Valid && passwordValid) {
+ errors.password2 = null
+ }
+ break
+ case 'password2':
+ password2Valid = value === this.state.password
+ errors.password2 = password2Valid ? '' : "Password don't match"
+ break
+ default:
+ break
+ }
+ this.setState(
+ {
+ errors,
+ passwordValid,
+ password2Valid
+ },
+ this.validateForm
+ )
+ }
+
+ validateForm = () => {
+ this.setState({
+ formValid: this.state.passwordValid && this.state.password2Valid
+ })
+ }
+ closeToast = () => {
+ // Remove error from store
+ this.props.updateError({})
+ this.setState({ openToast: false })
+ }
+
+ onSubmit = e => {
+ e.preventDefault()
+ // Remove error from store
+ const { password, password2, verificationToken } = this.state
+
+ if (password === password2) {
+ const data = {
+ password,
+ password2,
+ verificationToken
+ }
+ this.props.resetPassword(data, this.props.history)
+ } else {
+ const passwordError = 'Password dont match'
+ const errors = {
+ password2: password !== password2 ? passwordError : ''
+ }
+ this.props.updateError(errors)
+ }
+ }
+
+ render() {
+ const { errors, openToast } = this.state
+ return (
+
+
+
+ {errors.fetch ? (
+
+ ) : null}
+
+
+
+
+ Reset Password
+
+
+
+
+
+ )
+ }
+}
+
+Register.propTypes = {
+ resetPassword: PropTypes.func.isRequired,
+ auth: PropTypes.object.isRequired,
+ errors: PropTypes.object.isRequired,
+ history: PropTypes.object,
+ classes: PropTypes.object,
+ updateError: PropTypes.func.isRequired,
+ match: PropTypes.object.isRequired
+}
+
+const mapStateToProps = state => ({
+ auth: state.auth,
+ errors: state.errors
+})
+
+export default compose(
+ withStyles(styles, {
+ name: 'Register'
+ }),
+ connect(mapStateToProps, { resetPassword, updateError })
+)(Register)
diff --git a/fact-bounty-client/src/pages/ResetPassword/ResetPassword.style.js b/fact-bounty-client/src/pages/ResetPassword/ResetPassword.style.js
new file mode 100644
index 00000000..4af949a6
--- /dev/null
+++ b/fact-bounty-client/src/pages/ResetPassword/ResetPassword.style.js
@@ -0,0 +1,32 @@
+export default theme => ({
+ main: {
+ width: 'auto',
+ display: 'block', // Fix IE 11 issue.
+ marginLeft: theme.spacing.unit * 3,
+ marginRight: theme.spacing.unit * 3,
+ [theme.breakpoints.up(400 + theme.spacing.unit * 3 * 2)]: {
+ width: 400,
+ marginLeft: 'auto',
+ marginRight: 'auto'
+ }
+ },
+ paper: {
+ marginTop: theme.spacing.unit * 12,
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ padding: `${theme.spacing.unit * 2}px ${theme.spacing.unit * 3}px ${theme
+ .spacing.unit * 3}px`
+ },
+ avatar: {
+ margin: theme.spacing.unit,
+ backgroundColor: theme.palette.secondary.main
+ },
+ form: {
+ width: '100%', // Fix IE 11 issue.
+ marginTop: theme.spacing.unit
+ },
+ submit: {
+ marginTop: theme.spacing.unit * 3
+ }
+})
diff --git a/fact-bounty-client/src/pages/ResetPassword/index.js b/fact-bounty-client/src/pages/ResetPassword/index.js
new file mode 100644
index 00000000..6b2df239
--- /dev/null
+++ b/fact-bounty-client/src/pages/ResetPassword/index.js
@@ -0,0 +1,2 @@
+import ResetPassword from './ResetPassword'
+export default ResetPassword
diff --git a/fact-bounty-client/src/redux/actions/actionTypes.js b/fact-bounty-client/src/redux/actions/actionTypes.js
index 5364673b..ea705304 100644
--- a/fact-bounty-client/src/redux/actions/actionTypes.js
+++ b/fact-bounty-client/src/redux/actions/actionTypes.js
@@ -2,6 +2,8 @@ export const GET_ERRORS = 'GET_ERRORS'
export const UPDATE_ERRORS = 'UPDATE_ERRORS'
export const USER_LOADING = 'USER_LOADING'
export const SET_CURRENT_USER = 'SET_CURRENT_USER'
+export const GET_SUCCESS = 'GET_SUCCESS'
+export const UPDATE_SUCCESS = 'UPDATE_SUCCESS'
export const LOADING_POSTS = 'LOADING_POSTS'
export const FETCH_POSTS = 'FETCH_POSTS'
diff --git a/fact-bounty-client/src/redux/actions/authActions.js b/fact-bounty-client/src/redux/actions/authActions.js
index f6adc61c..462b61cb 100644
--- a/fact-bounty-client/src/redux/actions/authActions.js
+++ b/fact-bounty-client/src/redux/actions/authActions.js
@@ -1,6 +1,11 @@
import jwt_decode from 'jwt-decode'
import { setAuthToken, saveAllTokens } from '../../helpers/AuthTokenHelper'
-import { SET_CURRENT_USER, USER_LOADING, GET_ERRORS } from './actionTypes'
+import {
+ SET_CURRENT_USER,
+ USER_LOADING,
+ GET_ERRORS,
+ GET_SUCCESS
+} from './actionTypes'
import AuthService from '../../services/AuthService'
// Set logged in user
@@ -134,3 +139,91 @@ export const logoutUser = () => dispatch => {
console.error(err)
})
}
+
+export const forgotPassword = userData => dispatch => {
+ AuthService.forgotPassword(userData)
+ .then(res => {
+ if (res.status === 202) {
+ dispatch({
+ type: GET_ERRORS,
+ payload: { message: res.data.message }
+ })
+ }
+ if (res.status === 200) {
+ dispatch({
+ type: GET_SUCCESS,
+ payload: { message: res.data.message }
+ })
+ }
+ })
+ .catch(err => {
+ if (err && err.response) {
+ let payload = err.response.data
+ if (typeof payload === 'string') {
+ payload = { fetch: err.response.data }
+ }
+ dispatch({
+ type: GET_ERRORS,
+ payload
+ })
+ }
+ })
+}
+
+export const authVerificationToken = (userData, history) => dispatch => {
+ AuthService.authVerificationToken(userData)
+ .then(res => {
+ if (res.status === 202) {
+ dispatch({
+ type: GET_ERRORS,
+ payload: { message: res.data.message }
+ })
+ }
+ if (res.status === 200) {
+ history.push('/resetpassword/' + userData.verification_token)
+ }
+ })
+ .catch(err => {
+ if (err && err.response) {
+ let payload = err.response.data
+ if (typeof payload === 'string') {
+ payload = { fetch: err.response.data }
+ }
+ dispatch({
+ type: GET_ERRORS,
+ payload
+ })
+ }
+ })
+}
+
+export const resetPassword = (userData, history) => dispatch => {
+ AuthService.resetPassword(userData)
+ .then(res => {
+ if (res.status === 202) {
+ dispatch({
+ type: GET_ERRORS,
+ payload: { message: res.data.message }
+ })
+ }
+ if (res.status === 200) {
+ dispatch({
+ type: GET_SUCCESS,
+ payload: { message: res.data.message }
+ })
+ history.push('/login')
+ }
+ })
+ .catch(err => {
+ if (err && err.response) {
+ let payload = err.response.data
+ if (typeof payload === 'string') {
+ payload = { fetch: err.response.data }
+ }
+ dispatch({
+ type: GET_ERRORS,
+ payload
+ })
+ }
+ })
+}
diff --git a/fact-bounty-client/src/redux/actions/successActions.js b/fact-bounty-client/src/redux/actions/successActions.js
new file mode 100644
index 00000000..8f40ed74
--- /dev/null
+++ b/fact-bounty-client/src/redux/actions/successActions.js
@@ -0,0 +1,4 @@
+import { UPDATE_SUCCESS } from './actionTypes'
+
+export const updateSuccess = payload => dispatch =>
+ dispatch({ type: UPDATE_SUCCESS, payload })
diff --git a/fact-bounty-client/src/redux/reducers/index.js b/fact-bounty-client/src/redux/reducers/index.js
index ef358a13..ea3e30d4 100644
--- a/fact-bounty-client/src/redux/reducers/index.js
+++ b/fact-bounty-client/src/redux/reducers/index.js
@@ -4,10 +4,12 @@ import errorReducer from './errorReducers'
import postReducer from './postReducers'
import contactUsReducer from './contactUsReducers'
import twitterReducer from './twitterReducers'
+import successReducers from './successReducers'
export default combineReducers({
auth: authReducer,
errors: errorReducer,
+ success: successReducers,
posts: postReducer,
contactUs: contactUsReducer,
tweets: twitterReducer
diff --git a/fact-bounty-client/src/redux/reducers/successReducers.js b/fact-bounty-client/src/redux/reducers/successReducers.js
new file mode 100644
index 00000000..b3fda27e
--- /dev/null
+++ b/fact-bounty-client/src/redux/reducers/successReducers.js
@@ -0,0 +1,16 @@
+import { GET_SUCCESS, UPDATE_SUCCESS } from '../actions/actionTypes'
+
+const initialState = {}
+
+export default function(state = initialState, action) {
+ switch (action.type) {
+ case GET_SUCCESS:
+ return action.payload
+
+ case UPDATE_SUCCESS:
+ return action.payload
+
+ default:
+ return state
+ }
+}
diff --git a/fact-bounty-client/src/services/AuthService.js b/fact-bounty-client/src/services/AuthService.js
index 460b2912..3ecd4f4d 100644
--- a/fact-bounty-client/src/services/AuthService.js
+++ b/fact-bounty-client/src/services/AuthService.js
@@ -17,7 +17,30 @@ const loginUser = userData => {
const registerUser = userData => {
return ApiBuilder.API.post(`/api/users/register`, userData)
}
-
+/**
+ *
+ * POST : forgotPassword
+ *
+ */
+const forgotPassword = userData => {
+ return ApiBuilder.API.post(`/api/users/forgot_password`, userData)
+}
+/**
+ *
+ * POST : authVerificationToken
+ *
+ */
+const authVerificationToken = userData => {
+ return ApiBuilder.API.post(`/api/users/auth_verification_token`, userData)
+}
+/**
+ *
+ * POST : resetPassword
+ *
+ */
+const resetPassword = userData => {
+ return ApiBuilder.API.post(`/api/users/reset_password`, userData)
+}
/**
*
* POST : OauthUser
@@ -56,6 +79,9 @@ const revokeRefreshToken = () => {
export default {
loginUser,
registerUser,
+ forgotPassword,
+ authVerificationToken,
+ resetPassword,
OauthUser,
tokenRefresh,
revokeAccessToken,
diff --git a/fact-bounty-flask/api/app.py b/fact-bounty-flask/api/app.py
index f8c360d2..ffd4f2d6 100644
--- a/fact-bounty-flask/api/app.py
+++ b/fact-bounty-flask/api/app.py
@@ -15,8 +15,8 @@
def create_app(config_name):
+ # create and configure the app
try:
- # create and configure the app
app = Flask(
__name__,
static_folder="../build/static",
@@ -82,6 +82,7 @@ def register_commands(app):
app.cli.add_command(commands.clean)
app.cli.add_command(commands.urls)
app.cli.add_command(commands.deploy)
+ app.cli.add_command(commands.create_admin)
def register_shellcontext(app):
diff --git a/fact-bounty-flask/api/commands.py b/fact-bounty-flask/api/commands.py
index e99267ad..1f9d12e5 100644
--- a/fact-bounty-flask/api/commands.py
+++ b/fact-bounty-flask/api/commands.py
@@ -6,16 +6,39 @@
import coverage
import sys
import click
-from flask import current_app
+from flask import current_app, make_response, jsonify
from flask.cli import with_appcontext
from flask_migrate import upgrade
from werkzeug.exceptions import MethodNotAllowed, NotFound
+from .user import model
+import getpass
+import re
COV = None
if os.environ.get("FLASK_COVERAGE"):
COV = coverage.coverage(branch=True, include="./*")
COV.start()
+"""
+regex to check valid email is entered
+"""
+regex = r"^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$"
+
+"""
+function for
+for validating an Email
+"""
+
+
+def check(email):
+ """
+ pass the regualar expression
+ and the string in search() method
+ """
+ if re.search(regex, email):
+ return True
+ else:
+ return False
@click.command()
@@ -168,3 +191,32 @@ def deploy():
migrate database to latest revision
"""
upgrade()
+
+
+@click.command(name="create_admin")
+@with_appcontext
+def create_admin():
+ """
+ create an admin user
+ """
+ admin_username = input("Enter the admin username: ")
+ admin_password = getpass.getpass(prompt="Enter the admin password: ")
+ admin_role = "admin"
+ valid = False
+ while not valid:
+ admin_email = input("Enter the admin email: ")
+ valid = check(admin_email)
+ if valid:
+ break
+ else:
+ pass
+ try:
+ user = model.User(
+ name=admin_username,
+ email=admin_email,
+ password=admin_password,
+ role=admin_role,
+ )
+ user.save()
+ except Exception as e:
+ return {"Error occured": str(e)}
diff --git a/fact-bounty-flask/api/data-dev.sqlite b/fact-bounty-flask/api/data-dev.sqlite
new file mode 100644
index 00000000..08de9227
Binary files /dev/null and b/fact-bounty-flask/api/data-dev.sqlite differ
diff --git a/fact-bounty-flask/api/helpers.py b/fact-bounty-flask/api/helpers.py
index 96c0d0d3..9778d726 100644
--- a/fact-bounty-flask/api/helpers.py
+++ b/fact-bounty-flask/api/helpers.py
@@ -11,8 +11,11 @@ def send_async_email(app, msg):
def send_email(to, subject, body):
app = current_app._get_current_object()
- msg = Message(app.config['FACTBOUNTY_MAIL_SUBJECT_PREFIX'] + subject,
- sender=app.config['FACTBOUNTY_MAIL_SENDER'], recipients=[to])
+ msg = Message(
+ app.config["FACTBOUNTY_MAIL_SUBJECT_PREFIX"] + subject,
+ sender=app.config["FACTBOUNTY_MAIL_SENDER"],
+ recipients=[to],
+ )
msg.body = body
thr = Thread(target=send_async_email, args=[app, msg])
thr.start()
diff --git a/fact-bounty-flask/api/stories/controller.py b/fact-bounty-flask/api/stories/controller.py
index 5855eb05..05bbd45f 100644
--- a/fact-bounty-flask/api/stories/controller.py
+++ b/fact-bounty-flask/api/stories/controller.py
@@ -4,41 +4,49 @@
from elasticsearch.helpers import scan
from .model import Vote, Comment
from flasgger import swag_from
+from ..user import model
class AllStories(MethodView):
"""
- Retrieve stories
+ Retrieve stories only for admin
:return: JSON object with all stories and HTTP status code 200.
"""
+ @jwt_required
@swag_from("../../docs/stories/get_all.yml")
def get(self):
+ user_id = get_jwt_identity()
+ user = model.User.find_by_user_id(user_id)
- es_index = current_app.config["ES_INDEX"]
- es = current_app.elasticsearch
+ if user.role == "admin":
+ es_index = current_app.config["ES_INDEX"]
+ es = current_app.elasticsearch
- doc = {
- "sort": [{"date": {"order": "desc"}}],
- "query": {"match_all": {}},
- }
- stories = {}
- try:
- for story in scan(es, doc, index=es_index, doc_type="story"):
- PID = story["_id"]
- source = story["_source"]
- stories[PID] = source
- except Exception as e:
- # An error occured, so return a string message containing error
- response = {"message": str(e)}
- return make_response(jsonify(response)), 500
+ doc = {
+ "sort": [{"date": {"order": "desc"}}],
+ "query": {"match_all": {}},
+ }
+ stories = {}
+ try:
+ for story in scan(es, doc, index=es_index, doc_type="story"):
+ PID = story["_id"]
+ source = story["_source"]
+ stories[PID] = source
+ except Exception as e:
+ # An error occured, so return a string message containing error
+ response = {"message": str(e)}
+ return make_response(jsonify(response)), 500
- response = {
- "message": "Stories successfully fetched",
- "stories": stories,
- }
- return make_response(jsonify(response)), 200
+ response = {
+ "message": "Stories successfully fetched",
+ "stories": stories,
+ }
+ return make_response(jsonify(response)), 200
+ else:
+ response = {"message": "only for admins"}
+ return make_response(jsonify(response)), 400
class GetRange(MethodView):
diff --git a/fact-bounty-flask/api/user/controller.py b/fact-bounty-flask/api/user/controller.py
index 0f1485e6..2bd75087 100644
--- a/fact-bounty-flask/api/user/controller.py
+++ b/fact-bounty-flask/api/user/controller.py
@@ -1,5 +1,5 @@
from flask.views import MethodView
-from flask import make_response, request, jsonify
+from flask import make_response, request, jsonify, current_app
from flask_jwt_extended import (
create_access_token,
create_refresh_token,
@@ -10,11 +10,15 @@
)
from .model import User, RevokedToken
from flasgger import swag_from
+import smtplib
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
class Register(MethodView):
"""This class registers a new user."""
+ @swag_from("../../docs/users/register.yml")
def post(self):
"""Handle POST request for this view. Url --> /api/users/register"""
# getting JSON data from request
@@ -50,7 +54,8 @@ def post(self):
try:
user = User(email=email, password=password, name=name)
user.save()
- except Exception:
+ except Exception as err:
+ print("Error occured: ", err)
response = {"message": "Something went wrong!!"}
return make_response(jsonify(response)), 500
@@ -72,7 +77,8 @@ def post(self):
try:
email = data["email"]
password = data["password"]
- except Exception:
+ except Exception as err:
+ print("Error occured: ", err)
response = {"message": "Please provide all the required fields."}
return make_response(jsonify(response)), 404
@@ -109,10 +115,134 @@ def post(self):
return make_response(jsonify(response)), 200
+class ForgotPassword(MethodView):
+ """This class sends a reset password mail."""
+
+ def post(self):
+ """Handle POST request for this view. Url --> /api/users/forgot_password"""
+ # getting JSON data from request
+ post_data = request.get_json(silent=True)
+
+ try:
+ email = post_data["email"]
+ except Exception:
+ response = {"message": "Please provide email."}
+ return make_response(jsonify(response)), 404
+
+ # Querying the database with requested email
+ user = User.find_by_email(email)
+ if user:
+ verification_token = user.verification_token
+ # Body of the email
+ mail_content = (
+ """Hi,
+
+ You are receiving this because you have requested to reset password for your account.
+
+ Here is you verification token: """
+ + verification_token
+ )
+ # The email addresses and password
+ sender_address = current_app.config["MAIL_USERNAME"]
+ sender_pass = current_app.config["MAIL_PASSWORD"]
+ receiver_address = user.email
+ # Setup the MIME
+ message = MIMEMultipart()
+ message["From"] = sender_address
+ message["To"] = receiver_address
+ message["Subject"] = "Password reset"
+ # The body and the attachments for the mail
+ message.attach(MIMEText(mail_content, "plain"))
+ # Create SMTP session for sending the mail
+ session = smtplib.SMTP(
+ current_app.config["MAIL_SERVER"],
+ current_app.config["MAIL_PORT"],
+ ) # use gmail with port
+ session.starttls() # enable security
+ session.login(
+ sender_address, sender_pass
+ ) # login with mail_id and password
+ text = message.as_string()
+ session.sendmail(sender_address, receiver_address, text)
+ session.quit()
+ print("Password reset email sent successfully")
+
+ response = {
+ "message": "Verification token has been sent to you on your registered email"
+ }
+
+ # return a response notifying the user that a password reset mail has
+ # been sent to registered email
+ return make_response(jsonify(response)), 200
+ else:
+ response = {"message": "Email not registered"}
+ return make_response(jsonify(response)), 202
+
+
+class AuthVerificationToken(MethodView):
+ """This class verifies token which is being used to reset the password"""
+
+ def post(self):
+ data = request.get_json(silent=True)
+ try:
+ verification_token = data["verification_token"]
+ except Exception:
+ response = {"message": "Please provide verification token."}
+ return make_response(jsonify(response)), 404
+
+ # since token is unique, find user by its verification token
+ # if it exist it means input token is correct
+ user = User.find_by_verification_token(verification_token)
+
+ if user:
+ response = {"message": "Auth success"}
+ return make_response(jsonify(response)), 200
+ else:
+ response = {"message": "Incorrect, please check again"}
+ return make_response(jsonify(response)), 202
+
+
+class ResetPassword(MethodView):
+ """This class is used to change the password"""
+
+ def post(self):
+
+ data = request.get_json(silent=True)
+ try:
+ verification_token = data["verificationToken"]
+ new_password = data["password"]
+ new_password_confirm = data["password2"]
+ except Exception:
+ response = {"message": "Please provide all required fields."}
+ return make_response(jsonify(response)), 202
+
+ # find user by token
+ user = User.find_by_verification_token(verification_token)
+
+ # if token is correct
+ if user:
+ # new password and confirm has to be equal
+ if new_password != new_password_confirm:
+ response = {
+ "message": "New password and confirm password does not match"
+ }
+ return make_response(jsonify(response)), 202
+ # overwrite old password with new password
+ else:
+ user.password = user.generate_password_hash(new_password)
+ user.save()
+ response = {"message": "Password successfully changed"}
+ return make_response(jsonify(response)), 200
+ else:
+ response = {"message": "Unauthorized"}
+ return make_response(jsonify(response)), 401
+
+
class Auth(MethodView):
"""This class-based view handles user register and access token generation \
via 3rd sources like facebook, google"""
+ @swag_from("../../docs/users/oauth.yml")
def post(self):
# Querying the database with requested email
data = request.get_json(silent=True)
@@ -172,6 +302,7 @@ def post(self):
class LogoutAccess(MethodView):
@jwt_required
+ @swag_from("../../docs/users/logout_access.yml")
def post(self):
jti = get_raw_jwt()["jti"]
try:
@@ -188,6 +319,7 @@ def post(self):
class LogoutRefresh(MethodView):
@jwt_refresh_token_required
+ @swag_from("../../docs/users/logout_refresh.yml")
def post(self):
jti = get_raw_jwt()["jti"]
try:
@@ -204,6 +336,7 @@ def post(self):
class TokenRefresh(MethodView):
@jwt_refresh_token_required
+ @swag_from("../../docs/users/token_refresh.yml")
def post(self):
current_user = get_jwt_identity()
access_token = create_access_token(identity=current_user, fresh=False)
@@ -215,11 +348,53 @@ def post(self):
return make_response(jsonify(response)), 200
+class Profile(MethodView):
+ """This class-based view handles retrieving and updating the current \
+ user's information"""
+
+ @staticmethod
+ @jwt_required
+ def get(self):
+ current_user = get_jwt_identity()
+
+ response = jsonify(current_user)
+ return make_response(response), 200
+
+ @jwt_required
+ def post(self):
+ try:
+ post_data = request.get_json(silent=True)
+ name = post_data["name"]
+ email = post_data["email"]
+ except Exception:
+ response = {"message": "Some user details are missing."}
+ return make_response(jsonify(response)), 404
+
+ # Querying the database to get the user to update
+ user = User.find_by_email(email)
+
+ if user:
+ user = user.update(email=email, name=name)
+ user.save()
+ response = {"message": "User has been updated."}
+ return make_response(jsonify(response)), 204
+
+ else:
+ response = {"message": "User not found. Please check your request."}
+ return make_response(jsonify(response)), 404
+
+
userController = {
"register": Register.as_view("register"),
"login": Login.as_view("login"),
+ "forgot_password": ForgotPassword.as_view("forgot_password"),
+ "auth_verification_token": AuthVerificationToken.as_view(
+ "auth_verification_token"
+ ),
+ "reset_password": ResetPassword.as_view("reset_password"),
"auth": Auth.as_view("auth"),
"logout_access": LogoutAccess.as_view("logout_access"),
"logout_refresh": LogoutRefresh.as_view("logout_refresh"),
"token_refresh": TokenRefresh.as_view("token_refresh"),
+ "profile": Profile.as_view("profile")
}
diff --git a/fact-bounty-flask/api/user/model.py b/fact-bounty-flask/api/user/model.py
old mode 100644
new mode 100755
index 8fb28dc7..8361befb
--- a/fact-bounty-flask/api/user/model.py
+++ b/fact-bounty-flask/api/user/model.py
@@ -1,6 +1,7 @@
from datetime import datetime
from flask import current_app
from flask_bcrypt import Bcrypt
+from uuid import uuid4
# from itsdangerous import (
# TimedJSONWebSignatureSerializer as Serializer,
@@ -18,20 +19,24 @@ class User(Model):
id = Column(db.Integer, primary_key=True)
name = Column(db.String(80), nullable=False)
password = Column(db.String(128))
+ verification_token = Column(db.String(128), nullable=False, unique=True)
email = Column(db.String(100), nullable=False, unique=True)
date = Column(db.DateTime, default=datetime.now())
votes = db.relationship("Vote", backref=db.backref("user"))
type = Column(db.String(50), default="remote")
+ role = Column(db.String(10), default="user")
- def __init__(self, name, email, password, _type="remote"):
+ def __init__(self, name, email, password, role="user", _type="remote"):
"""
Initializes the user instance
"""
self.name = name
self.email = email
+ self.verification_token = User.generate_token()
if password:
- self.password = Bcrypt().generate_password_hash(password).decode()
+ self.password = User.generate_password_hash(password)
self.type = _type
+ self.role = role
def __repr__(self):
"""
@@ -45,13 +50,42 @@ def to_json(self):
:return: user JSON object
"""
- user_json = {"name": self.name, "email": self.email, "date": self.date}
+ user_json = {
+ "name": self.name,
+ "email": self.email,
+ "date": self.date,
+ "role": self.role,
+ }
return user_json
@classmethod
def find_by_email(cls, email):
return cls.query.filter_by(email=email).first()
+ @classmethod
+ def find_by_user_id(cls, id):
+ return cls.query.filter_by(id=id).first()
+
+ @classmethod
+ def find_by_verification_token(cls, verification_token):
+ return cls.query.filter_by(
+ verification_token=verification_token
+ ).first()
+
+ @staticmethod
+ def generate_token():
+ """
+ Returns a random token
+ """
+ return uuid4().hex
+
+ @staticmethod
+ def generate_password_hash(password):
+ """
+ Returns hash of password
+ """
+ return Bcrypt().generate_password_hash(password).decode()
+
def verify_password(self, password):
"""
Verify the password
@@ -71,7 +105,7 @@ def save(self):
class RevokedToken(Model):
"""
- This model holds information about revoked tokens, users who have looged out
+ This model holds information about revoked tokens, users who have logged out
"""
__tablename__ = "revoked_tokens"
diff --git a/fact-bounty-flask/api/user/views.py b/fact-bounty-flask/api/user/views.py
index ea9213da..20ddefc3 100644
--- a/fact-bounty-flask/api/user/views.py
+++ b/fact-bounty-flask/api/user/views.py
@@ -10,6 +10,23 @@
userprint.add_url_rule(
"/register", view_func=userController["register"], methods=["POST"]
)
+userprint.add_url_rule(
+ "/forgot_password",
+ view_func=userController["forgot_password"],
+ methods=["POST"],
+)
+
+userprint.add_url_rule(
+ "/auth_verification_token",
+ view_func=userController["auth_verification_token"],
+ methods=["POST"],
+)
+
+userprint.add_url_rule(
+ "/reset_password",
+ view_func=userController["reset_password"],
+ methods=["POST"],
+)
userprint.add_url_rule(
"/oauth", view_func=userController["auth"], methods=["POST"]
@@ -32,3 +49,9 @@
view_func=userController["token_refresh"],
methods=["POST"],
)
+
+userprint.add_url_rule(
+ "/profile",
+ view_func=userController["profile"],
+ methods=["GET", "POST"]
+)
diff --git a/fact-bounty-flask/docs/users/login.yml b/fact-bounty-flask/docs/users/login.yml
index ba76f27b..f3faca2b 100644
--- a/fact-bounty-flask/docs/users/login.yml
+++ b/fact-bounty-flask/docs/users/login.yml
@@ -1,7 +1,7 @@
User login
---
tags:
-- Auth
+- User
consumes:
- "application/json"
parameters:
diff --git a/fact-bounty-flask/docs/users/logout_access.yml b/fact-bounty-flask/docs/users/logout_access.yml
new file mode 100644
index 00000000..47d991c3
--- /dev/null
+++ b/fact-bounty-flask/docs/users/logout_access.yml
@@ -0,0 +1,17 @@
+User logout
+---
+tags:
+- User
+produces:
+ - application/json
+parameters:
+- in: header
+ name: Authorization
+ description: an authorization header
+ required: true
+ type: string
+responses:
+ 200:
+ description: Access token has been revoked.
+ 500:
+ description: Something went wrong!
diff --git a/fact-bounty-flask/docs/users/logout_refresh.yml b/fact-bounty-flask/docs/users/logout_refresh.yml
new file mode 100644
index 00000000..e5c4e3b5
--- /dev/null
+++ b/fact-bounty-flask/docs/users/logout_refresh.yml
@@ -0,0 +1,17 @@
+User logout
+---
+tags:
+- User
+produces:
+ - application/json
+parameters:
+- in: header
+ name: Authorization
+ description: an authorization header
+ required: true
+ type: string
+responses:
+ 200:
+ description: Refresh token has been revoked.
+ 500:
+ description: Something went wrong!
diff --git a/fact-bounty-flask/docs/users/oauth.yml b/fact-bounty-flask/docs/users/oauth.yml
new file mode 100644
index 00000000..b88f2bee
--- /dev/null
+++ b/fact-bounty-flask/docs/users/oauth.yml
@@ -0,0 +1,69 @@
+User registration via OAuth like facebook, google, etc.
+---
+tags:
+ - User
+consumes:
+ - "application/json"
+produces:
+ - "application/json"
+parameters:
+- in: body
+ name: body
+ schema:
+ type: object
+ properties:
+ name:
+ type: string
+ example: xyz
+ email:
+ type: string
+ example: xyz@gmail.com
+ type:
+ type: string
+ example: admin
+responses:
+ 201:
+ description: (for new user) JSON object containing success message, access token and refresh token.
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ example: You logged in successfully.
+ access_token:
+ type: string
+ example: hash-generated-by-jwt
+ refresh_token:
+ type: string
+ example: hash-generated-by-jwt
+ 202:
+ description: (for existing user) JSON object containing success message, access token and refresh token.
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ example: You logged in successfully.
+ access_token:
+ type: string
+ example: hash-generated-by-jwt
+ refresh_token:
+ type: string
+ example: hash-generated-by-jwt
+
+ 404:
+ description: Fields are missing
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ example: Please provide all the required fields.
+ 500:
+ description: Something went wrong
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ example: Something went wrong
diff --git a/fact-bounty-flask/docs/users/register.yml b/fact-bounty-flask/docs/users/register.yml
new file mode 100644
index 00000000..6c042f36
--- /dev/null
+++ b/fact-bounty-flask/docs/users/register.yml
@@ -0,0 +1,67 @@
+User registration via form
+---
+tags:
+ - User
+consumes:
+ - "application/json"
+produces:
+ - "application/json"
+parameters:
+- in: body
+ name: body
+ schema:
+ type: object
+ properties:
+ name:
+ type: string
+ example: xyz
+ email:
+ type: string
+ example: xyz@gmail.com
+ password:
+ type: string
+ example: password123
+ password2:
+ type: string
+ example: password123
+responses:
+ 201:
+ description: JSON object containing success message
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ example: You registered successfully. Please log in.
+ 202:
+ description: JSON object containing success message
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ example: User already exists. Please login.
+ 401:
+ description: Passwords do not match
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ example: Both passwords does not match
+ 404:
+ description: Fields are missing
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ example: Please provide all the required fields.
+ 500:
+ description: Something went wrong
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ example: Something went wrong
diff --git a/fact-bounty-flask/docs/users/token_refresh.yml b/fact-bounty-flask/docs/users/token_refresh.yml
new file mode 100644
index 00000000..1176c67b
--- /dev/null
+++ b/fact-bounty-flask/docs/users/token_refresh.yml
@@ -0,0 +1,15 @@
+Token refresh
+---
+tags:
+- User
+produces:
+ - application/json
+parameters:
+- in: header
+ name: Authorization
+ description: an authorization header
+ required: true
+ type: string
+responses:
+ 200:
+ description: Token refreshed successfully.
diff --git a/fact-bounty-flask/migrations/versions/02db7a3aaa95_.py b/fact-bounty-flask/migrations/versions/02db7a3aaa95_.py
new file mode 100644
index 00000000..a10bf7ff
--- /dev/null
+++ b/fact-bounty-flask/migrations/versions/02db7a3aaa95_.py
@@ -0,0 +1,66 @@
+"""empty message
+
+Revision ID: 02db7a3aaa95
+Revises: 78d18dc2789d
+Create Date: 2020-01-25 11:13:21.447023
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = "02db7a3aaa95"
+down_revision = "78d18dc2789d"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table(
+ "revoked_tokens",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("jti", sa.String(length=120), nullable=True),
+ sa.PrimaryKeyConstraint("id"),
+ )
+ op.create_table(
+ "user",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("name", sa.String(length=80), nullable=False),
+ sa.Column("password", sa.String(length=128), nullable=True),
+ sa.Column("email", sa.String(length=100), nullable=False),
+ sa.Column("date", sa.DateTime(), nullable=True),
+ sa.Column("type", sa.String(length=50), nullable=True),
+ sa.Column("role", sa.String(length=60), nullable=True),
+ sa.PrimaryKeyConstraint("id"),
+ sa.UniqueConstraint("email"),
+ )
+ op.create_table(
+ "comment",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("story_id", sa.String(length=128), nullable=True),
+ sa.Column("user_id", sa.Integer(), nullable=True),
+ sa.Column("content", sa.Integer(), nullable=True),
+ sa.ForeignKeyConstraint(["user_id"], ["user.id"],),
+ sa.PrimaryKeyConstraint("id"),
+ )
+ op.create_table(
+ "vote",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("story_id", sa.String(length=128), nullable=True),
+ sa.Column("user_id", sa.Integer(), nullable=True),
+ sa.Column("value", sa.Integer(), nullable=True),
+ sa.ForeignKeyConstraint(["user_id"], ["user.id"],),
+ sa.PrimaryKeyConstraint("id"),
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table("vote")
+ op.drop_table("comment")
+ op.drop_table("user")
+ op.drop_table("revoked_tokens")
+ # ### end Alembic commands ###
diff --git a/fact-bounty-flask/migrations/versions/78d18dc2789d_.py b/fact-bounty-flask/migrations/versions/78d18dc2789d_.py
new file mode 100644
index 00000000..1d7f5203
--- /dev/null
+++ b/fact-bounty-flask/migrations/versions/78d18dc2789d_.py
@@ -0,0 +1,58 @@
+"""empty message
+
+Revision ID: 78d18dc2789d
+Revises: d2fc5d7ff47e
+Create Date: 2020-01-25 10:50:19.720464
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = "78d18dc2789d"
+down_revision = "d2fc5d7ff47e"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table(
+ "revoked_tokens",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("jti", sa.String(length=120), nullable=True),
+ sa.PrimaryKeyConstraint("id"),
+ )
+ op.create_table(
+ "comment",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("story_id", sa.String(length=128), nullable=True),
+ sa.Column("user_id", sa.Integer(), nullable=True),
+ sa.Column("content", sa.Integer(), nullable=True),
+ sa.ForeignKeyConstraint(["user_id"], ["user.id"],),
+ sa.PrimaryKeyConstraint("id"),
+ )
+ op.add_column(
+ "user", sa.Column("role", sa.String(length=60), nullable=True)
+ )
+ op.alter_column(
+ "user", "password", existing_type=sa.VARCHAR(length=128), nullable=True
+ )
+ op.create_unique_constraint(None, "user", ["email"])
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_constraint(None, "user", type_="unique")
+ op.alter_column(
+ "user",
+ "password",
+ existing_type=sa.VARCHAR(length=128),
+ nullable=False,
+ )
+ op.drop_column("user", "role")
+ op.drop_table("comment")
+ op.drop_table("revoked_tokens")
+ # ### end Alembic commands ###
diff --git a/fact-bounty-flask/requirements.txt b/fact-bounty-flask/requirements.txt
index 08ad1896..734b3fff 100644
--- a/fact-bounty-flask/requirements.txt
+++ b/fact-bounty-flask/requirements.txt
@@ -43,7 +43,7 @@ SQLAlchemy==1.2.18
tornado==6.0
tweepy==3.8.0
typed-ast==1.2.0
-urllib3==1.24.1
+urllib3==1.23.0
Werkzeug==0.14.1
wrapt==1.11.1
WTForms==2.2.1