security
module conveniently implements secure password hashinggenerate_password_hash(password, method="pbkdf2:sha256", salt_length=8)
check_password_hash(hash, password)
(venv) $ python
>>> from werkzeug.security import generate_password_hash, check_password_hash
>>> password="123"
>>> password_hash=generate_password_hash(password, method="pbkdf2:sha1")
>>> password_hash
'pbkdf2:sha1:50000$iO1WliCS$19c136df89c10115bf1e983e57c060f9345452c3
>>> check_password_hash(password_hash, password)
True
>>> check_password_hash(password_hash, "12")
False
# app/models.py
from werkzeug.security import generate_password_hash, check_password_hash
...
class User(db.Model):
...
email = db.Column(db.String(64), unique=True, index=True)
password_hash = db.Column(db.String(128))
@property
def password(self):
raise AttributeError('password is not a readable attribute')
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password, method="pbkdf2:sha1")
def verify_password(self, password):
return check_password_hash(self.password_hash, password)
...
@property
, please refer to http://tw.pyladies.com/~maomao/4_effective_python.slides.html#/1(venv) $ python manage.py shell
>>> u = User()
>>> u.password = 'cat'
>>> u.verify_password('cat')
True
>>> u.verify_password('dog')
False
>>> u2 = User()
>>> u2.password = 'cat'
>>> u.password_hash == u2.password_hash
False
>>> u.password_hash
'pbkdf2:sha1:50000$4DzKb2yV$f1e49b38a34d0726310016c59fcc88f509e40ce8'
>>> u2.password_hash
'pbkdf2:sha1:50000$cXR2Idzh$ecc84c8665d2646b78487ca34c9f7da32ae03c60'
# tests/test_user_model.py
import unittest
from app import create_app, db
from app.models import User
class UserModelTestCase(unittest.TestCase):
def setUp(self):
self.app = create_app('testing')
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all()
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
...
# tests/test_user_model.py
...
class UserModelTestCase(unittest.TestCase):
...
def test_password_setter(self):
u = User(password='cat')
self.assertTrue(u.password_hash is not None)
def test_no_password_getter(self):
u = User(password='cat')
with self.assertRaises(AttributeError):
u.password
def test_password_verification(self):
u = User(password='cat')
self.assertTrue(u.verify_password('cat'))
self.assertFalse(u.verify_password('dog'))
def test_password_salts_are_random(self):
u = User(password='cat')
u2 = User(password='cat')
self.assertTrue(u.password_hash != u2.password_hash)
$
git clone https://github.com/win911/flask_class.git$
git checkout 8a$
python manage.py db upgradeauth
blueprint# app/auth/__init__.py
from flask import Blueprint
auth = Blueprint('auth', __name__)
from . import views
# app/auth/views.py
from flask import render_template
from . import auth
@auth.route('/login')
def login():
return render_template('auth/login.html')
<!-- app/templates/auth/login.html -->
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Login</h1>
</div>
{% endblock %}
# app/__init__.py
...
def create_app(config_name):
...
# blueprint registration
...
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint, url_prefix='/auth')
...
$
git clone https://github.com/win911/flask_class.git$
git checkout 8bMethod | Description |
---|---|
is_authenticated() | Must return True if the user has login credentials or False otherwise. |
is_active() | Must return True if the user is allowed to log in or False otherwise. A False return value can be used for disabled accounts. |
is_anonymous() | Must always return False for regular users |
get_id() | Must return a unique identifier for the user, encoded as a Unicode string |
UserMixin
class# app/models.py
from flask_login import UserMixin
...
class User(UserMixin, db.Model):
...
# app/__init__.py
...
from flask_login import LoginManager
...
login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'auth.login'
def create_app(config_name):
...
login_manager.init_app(app)
# blueprint registration
...
None
otherwise# app/models.py
...
from . import login_manager
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
...
# app/main/views.py
...
from flask_login import login_required
...
@main.route('/secret')
@login_required
def secret():
return 'Only authenticated users are allowed!'
$
git clone https://github.com/win911/flask_class.git$
git checkout 8c# app/auth/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import Required, Length, Email
class LoginForm(FlaskForm):
email = StringField('Email', validators=[Required(), Length(1, 64), Email()])
password = PasswordField('Password', validators=[Required()])
remember_me = BooleanField('Keep me logged in')
submit = SubmitField('Log In')
# app/auth/views.py
from flask import render_template, redirect, request, url_for, flash
from flask_login import login_user
from . import auth
from ..models import User
from .forms import LoginForm
@auth.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user is not None and user.verify_password(form.password.data):
login_user(user, form.remember_me.data)
return redirect(request.args.get('next') or url_for('main.index'))
flash('Invalid username or password.')
return render_template('auth/login.html', form=form)
<!-- app/templates/auth/login.html -->
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky{% endblock %}
{% block content %}
<div class="container">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">×</button>
{{ message }}
</div>
{% endfor %}
{% block page_content %}
<div class="page-header">
<h1>Login</h1>
</div>
{{ wtf.quick_form(form) }}
{% endblock %}
</div>
{% endblock %}
# app/main/views.py
from flask import render_template, session, redirect, url_for, current_app
from flask_login import login_required, current_user
from .. import db
from ..models import User
from ..email import send_email
from . import main
from .forms import NameForm
@main.route('/')
def index():
return render_template('index.html')
@main.route('/secret')
@login_required
def secret():
return 'Only authenticated users are allowed! Current user: {}'.format(current_user.username)
<!-- app/templates/index.html -->
{% extends "base.html" %}
{% block title %}Flasky{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Hello, {% if current_user.is_authenticated %}{{ current_user.username }}{% else %}Stranger{% endif %}!</h1>
</div>
{% endblock %}
False
(venv) $ python manage.py shell
>>> user = User(username="Maomao", email="maomao@tw.pyladies.com")
>>> user.password = "test"
>>> db.session.add(user)
>>> db.session.commit()
# app/auth/views.py
...
from flask_login import login_user, logout_user, login_required
...
@auth.route('/logout')
@login_required
def logout():
logout_user()
flash('You have been logged out.')
return redirect(url_for('main.index'))
<!-- app/templates/base.html -->
{% extends "bootstrap/base.html" %}
{% block title %}Flasky{% endblock %}
{% block head %}
{{ super() }}
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
{% endblock %}
...
...
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{ url_for('main.index') }}">Flasky</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="{{ url_for('main.index') }}">Home</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
{% if current_user.is_authenticated %}
<li><a href="{{ url_for('auth.logout') }}">Log Out</a></li>
{% else %}
<li><a href="{{ url_for('auth.login') }}">Log In</a></li>
{% endif %}
</ul>
</div>
</div>
</div>
{% endblock %}
...
...
{% block content %}
<div class="container">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">×</button>
{{ message }}
</div>
{% endfor %}
{% block page_content %}{% endblock %}
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{% endblock %}
$
git clone https://github.com/win911/flask_class.git$
git checkout 8d# app/auth/forms.py
...
from wtforms.validators import Required, Length, Email, Regexp, EqualTo
from wtforms import ValidationError
from ..models import User
class LoginForm(FlaskForm):
...
class RegistrationForm(FlaskForm):
email = StringField('Email', validators=[Required(), Length(1, 64),
Email()])
username = StringField('Username', validators=[Required(), Length(1, 64), Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,
'Usernames must have only letters, numbers, dots or underscores')])
password = PasswordField('Password', validators=[Required(), EqualTo('password2', message='Passwords must match.')])
password2 = PasswordField('Confirm password', validators=[Required()])
submit = SubmitField('Register')
def validate_email(self, field):
if User.query.filter_by(email=field.data).first():
raise ValidationError('Email already registered.')
def validate_username(self, field):
if User.query.filter_by(username=field.data).first():
raise ValidationError('Username already in use.')
validate_
followed by the name of a field# app/auth/views.py
...
from .forms import LoginForm, RegistrationForm
from .. import db
...
@auth.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
user = User(email=form.email.data, username=form.username.data, password=form.password.data)
db.session.add(user)
flash('You can now login.')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', form=form)
<!-- app/templates/auth/register.html -->
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Register{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Register</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
<!-- app/templates/auth/login.html -->
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Login{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Login</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
<br>
<p>New user? <a href="{{ url_for('auth.register') }}">Click here to register</a>.</p>
</div>
{% endblock %}
$
git clone https://github.com/win911/flask_class.git$
git checkout 8eitsdangerous
itsdangerous
can generate a secure token that contains a user id inside¶(venv) $ python manage.py shell
>>> from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
>>> s = Serializer(app.config['SECRET_KEY'], expires_in = 3600)
>>> token = s.dumps({'confirm': 23})
>>> token
b'eyJhbGciOiJIUzI1NiIsImlhdCI6MTUxMTYxNDA4OSwiZXhwIjoxNTExNjE3Njg5fQ.eyJjb25maXJtIjoyM30.P5qXhbnswX3LwMalPj-RBnOj3WmnsDJwT_TzN7CtVlw'
>>> data = s.loads(token)
>>> data
{'confirm': 23}
expires_in
argument sets an expiration time for the token expressed in seconds# app/models.py
...
from flask import current_app
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
...
class User(UserMixin, db.Model):
...
confirmed = db.Column(db.Boolean, default=False)
def generate_confirmation_token(self, expiration=3600):
s = Serializer(current_app.config['SECRET_KEY'], expiration)
return s.dumps({'confirm': self.id})
def confirm(self, token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return False
if data.get('confirm') != self.id:
return False
self.confirmed = True
db.session.add(self)
return True
...
# app/auth/views.py
...
from ..email import send_email
...
@auth.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
user = User(email=form.email.data, username=form.username.data, password=form.password.data)
db.session.add(user)
db.session.commit()
token = user.generate_confirmation_token()
send_email(user.email, 'Confirm Your Account', 'auth/email/confirm', user=user, token=token)
flash('A confirmation email has been sent to you by email.')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', form=form)
Dear {{ user.username }},
Welcome to Flasky!
To confirm your account please click on the following link:
{{ url_for('auth.confirm', token=token, _external=True) }}
Sincerely,
The Flasky Team
Note: replies to this email address are not monitored.
<!-- app/templates/auth/email/confirm.html -->
<p>Dear {{ user.username }},</p>
<p>Welcome to <b>Flasky</b>!</p>
<p>To confirm your account please <a href="{{ url_for('auth.confirm', token=token, _external=True) }}">click here</a>.</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ url_for('auth.confirm', token=token, _external=True) }}</p>
<p>Sincerely,</p>
<p>The Flasky Team</p>
<p><small>Note: replies to this email address are not monitored.</small></p>
# app/auth/views.py
...
from flask_login import current_user
...
@auth.route('/confirm/<token>')
@login_required
def confirm(token):
if current_user.confirmed:
return redirect(url_for('main.index'))
if current_user.confirm(token):
flash('You have confirmed your account. Thanks!')
else:
flash('The confirmation link is invalid or has expired.')
return redirect(url_for('main.index'))
$
python manage.py db upgrade# tests/test_user_model.py
import time
...
class UserModelTestCase(unittest.TestCase):
...
def test_valid_confirmation_token(self):
u = User(password='cat')
db.session.add(u)
db.session.commit()
token = u.generate_confirmation_token()
self.assertTrue(u.confirm(token))
def test_invalid_confirmation_token(self):
u1 = User(password='cat')
u2 = User(password='dog')
db.session.add(u1)
db.session.add(u2)
db.session.commit()
token = u1.generate_confirmation_token()
self.assertFalse(u2.confirm(token))
def test_expired_confirmation_token(self):
u = User(password='cat')
db.session.add(u)
db.session.commit()
token = u.generate_confirmation_token(1)
time.sleep(2)
self.assertFalse(u.confirm(token))
$
git clone https://github.com/win911/flask_class.git$
git checkout 8f$
python manage.py db upgradebefore_request
hook, which was briefly described in Chapter 2# app/auth/views.py
...
@auth.before_app_request
def before_request():
if current_user.is_authenticated \
and not current_user.confirmed \
and request.endpoint \
and request.endpoint[:5] != 'auth.' \
and request.endpoint != 'static':
return redirect(url_for('auth.unconfirmed'))
@auth.route('/unconfirmed')
def unconfirmed():
if current_user.is_anonymous or current_user.confirmed:
return redirect(url_for('main.index'))
return render_template('auth/unconfirmed.html')
<!-- app/templates/auth/unconfirmed.html -->
{% extends "base.html" %}
{% block title %}Flasky - Confirm your account{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>
Hello, {{ current_user.username }}!
</h1>
<h3>You have not confirmed your account yet.</h3>
<p>
Before you can access this site you need to confirm your account.
Check your inbox, you should have received an email with a confirmation link.
</p>
<p>
Need another confirmation email?
<a href="{{ url_for('auth.resend_confirmation') }}">Click here</a>
</p>
</div>
{% endblock %}
# app/auth/views.py
...
@auth.route('/confirm')
@login_required
def resend_confirmation():
token = current_user.generate_confirmation_token()
send_email(current_user.email, 'Confirm Your Account', 'auth/email/confirm', user=current_user, token=token)
flash('A new confirmation email has been sent to you by email.')
return redirect(url_for('main.index'))
$
git clone https://github.com/win911/flask_class.git$
git checkout 8g<li><a href="{{ url_for('auth.logout') }}">Log Out</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Account <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="{{ url_for('auth.change_password') }}">Change Password</a></li>
<li><a href="{{ url_for('auth.logout') }}">Log Out</a></li>
</ul>
</li>
# app/auth/forms.py
...
class ChangePasswordForm(FlaskForm):
old_password = PasswordField('Old password', validators=[Required()])
password = PasswordField('New password', validators=[Required(), EqualTo('password2', message='Passwords must match.')])
password2 = PasswordField('Confirm new password', validators=[Required()])
submit = SubmitField('Update Password')
<!-- app/templates/auth/change_password.html -->
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Change Password{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Change Your Password</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
# app/auth/views.py
...
@auth.route('/change-password', methods=['GET', 'POST'])
@login_required
def change_password():
# TODO: complete this function by yourself
pass
$
git clone https://github.com/win911/flask_class.git$
git checkout 8h<p>Forgot your password? <a href="{{ url_for('auth.password_reset_request') }}">Click here to reset it</a>.</p>
# app/auth/forms.py
...
class PasswordResetRequestForm(FlaskForm):
email = StringField('Email', validators=[Required(), Length(1, 64), Email()])
submit = SubmitField('Reset Password')
<!-- app/templates/auth/reset_password.html -->
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Password Reset{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Reset Your Password</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
Dear {{ user.username }},
To reset your password click on the following link:
{{ url_for('auth.password_reset', token=token, _external=True) }}
If you have not requested a password reset simply ignore this message.
Sincerely,
The Flasky Team
Note: replies to this email address are not monitored.
<!-- app/templates/auth/email/reset_password.html -->
<p>Dear {{ user.username }},</p>
<p>To reset your password <a href="{{ url_for('auth.password_reset', token=token, _external=True) }}">click here</a>.</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ url_for('auth.password_reset', token=token, _external=True) }}</p>
<p>If you have not requested a password reset simply ignore this message.</p>
<p>Sincerely,</p>
<p>The Flasky Team</p>
<p><small>Note: replies to this email address are not monitored.</small></p>
# app/models.py
...
class User(UserMixin, db.Model):
...
def generate_reset_token(self, expiration=3600):
s = Serializer(current_app.config['SECRET_KEY'], expiration)
return s.dumps({'reset': self.id}).decode('utf-8')
@staticmethod
def reset_password(token, new_password):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token.encode('utf-8'))
except:
return False
user = User.query.get(data.get('reset'))
if user is None:
return False
user.password = new_password
db.session.add(user)
return True
...
# app/auth/views.py
...
@auth.route('/reset', methods=['GET', 'POST'])
def password_reset_request():
# TODO: complete this function by yourself
pass
# app/auth/forms.py
...
class PasswordResetForm(FlaskForm):
password = PasswordField('New Password', validators=[Required(), EqualTo('password2', message='Passwords must match')])
password2 = PasswordField('Confirm password', validators=[Required()])
submit = SubmitField('Reset Password')
<
token>
"¶# app/auth/views.py
...
@auth.route('/reset/<token>', methods=['GET', 'POST'])
def password_reset(token):
# TODO: complete this function by yourself
pass
# tests/test_user_model.py
...
class UserModelTestCase(unittest.TestCase):
...
def test_valid_reset_token(self):
u = User(password='cat')
db.session.add(u)
db.session.commit()
token = u.generate_reset_token()
self.assertTrue(User.reset_password(token, 'dog'))
self.assertTrue(u.verify_password('dog'))
def test_invalid_reset_token(self):
u = User(password='cat')
db.session.add(u)
db.session.commit()
token = u.generate_reset_token()
self.assertFalse(User.reset_password(token + 'a', 'horse'))
self.assertTrue(u.verify_password('cat'))
$
git clone https://github.com/win911/flask_class.git$
git checkout 8i<li><a href="{{ url_for('auth.change_email_request') }}">Change Email</a></li>
# app/auth/forms.py
...
class ChangeEmailForm(FlaskForm):
email = StringField('New Email', validators=[Required(), Length(1, 64), Email()])
password = PasswordField('Password', validators=[Required()])
submit = SubmitField('Update Email Address')
def validate_email(self, field):
if User.query.filter_by(email=field.data).first():
raise ValidationError('Email already registered.')
<!-- app/templates/auth/change_email.html -->
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Change Email Address{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Change Your Email Address</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
Dear {{ user.username }},
To confirm your new email address click on the following link:
{{ url_for('auth.change_email', token=token, _external=True) }}
Sincerely,
The Flasky Team
Note: replies to this email address are not monitored.
<!-- app/templates/auth/email/change_email.html -->
<p>Dear {{ user.username }},</p>
<p>To confirm your new email address <a href="{{ url_for('auth.change_email', token=token, _external=True) }}">click here</a>.</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ url_for('auth.change_email', token=token, _external=True) }}</p>
<p>Sincerely,</p>
<p>The Flasky Team</p>
<p><small>Note: replies to this email address are not monitored.</small></p>
# app/models.py
...
class User(UserMixin, db.Model):
...
def generate_email_change_token(self, new_email, expiration=3600):
s = Serializer(current_app.config['SECRET_KEY'], expiration)
return s.dumps(
{'change_email': self.id, 'new_email': new_email}).decode('utf-8')
def change_email(self, token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token.encode('utf-8'))
except:
return False
if data.get('change_email') != self.id:
return False
new_email = data.get('new_email')
if new_email is None:
return False
if self.query.filter_by(email=new_email).first() is not None:
return False
self.email = new_email
db.session.add(self)
return True
# app/auth/views.py
...
@auth.route('/change_email', methods=['GET', 'POST'])
@login_required
def change_email_request():
# TODO: complete this function by yourself
pass
<
token>
"¶# app/auth/views.py
...
@auth.route('/change_email/<token>')
@login_required
def change_email(token):
# TODO: complete this function by yourself
pass
# tests/test_user_model.py
class UserModelTestCase(unittest.TestCase):
...
def test_valid_email_change_token(self):
u = User(email='john@example.com', password='cat')
db.session.add(u)
db.session.commit()
token = u.generate_email_change_token('susan@example.org')
self.assertTrue(u.change_email(token))
self.assertTrue(u.email == 'susan@example.org')
def test_invalid_email_change_token(self):
u1 = User(email='john@example.com', password='cat')
u2 = User(email='susan@example.org', password='dog')
db.session.add(u1)
db.session.add(u2)
db.session.commit()
token = u1.generate_email_change_token('david@example.net')
self.assertFalse(u2.change_email(token))
self.assertTrue(u2.email == 'susan@example.org')
def test_duplicate_email_change_token(self):
u1 = User(email='john@example.com', password='cat')
u2 = User(email='susan@example.org', password='dog')
db.session.add(u1)
db.session.add(u2)
db.session.commit()
token = u2.generate_email_change_token('john@example.com')
self.assertFalse(u2.change_email(token))
self.assertTrue(u2.email == 'susan@example.org')
$
git clone https://github.com/win911/flask_class.git$
git checkout 8j