Skip to content

Commit 8881a88

Browse files
committed
Initial Commit
0 parents  commit 8881a88

File tree

20 files changed

+616
-0
lines changed

20 files changed

+616
-0
lines changed

.gitignore

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
.DS_Store
2+
.env
3+
.flaskenv
4+
*.pyc
5+
*.pyo
6+
env/
7+
venv/
8+
.venv/
9+
env*
10+
dist/
11+
build/
12+
*.egg
13+
*.egg-info/
14+
.tox/
15+
.cache/
16+
.pytest_cache/
17+
.idea/
18+
docs/_build/
19+
.vscode
20+
instance/
21+
22+
# Coverage reports
23+
htmlcov/
24+
.coverage
25+
.coverage.*
26+
*,cover
27+
28+
migrations/

config.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from decouple import config
2+
3+
DATABASE_URI = config("DATABASE_URL")
4+
if DATABASE_URI.startswith("postgres://"):
5+
DATABASE_URI = DATABASE_URI.replace("postgres://", "postgresql://", 1)
6+
7+
8+
class Config(object):
9+
DEBUG = False
10+
TESTING = False
11+
CSRF_ENABLED = True
12+
SECRET_KEY = config("SECRET_KEY", default="guess-me")
13+
SQLALCHEMY_DATABASE_URI = DATABASE_URI
14+
SQLALCHEMY_TRACK_MODIFICATIONS = False
15+
BCRYPT_LOG_ROUNDS = 13
16+
WTF_CSRF_ENABLED = True
17+
DEBUG_TB_ENABLED = False
18+
DEBUG_TB_INTERCEPT_REDIRECTS = False
19+
APP_NAME = config("APP_NAME")
20+
21+
22+
class DevelopmentConfig(Config):
23+
DEVELOPMENT = True
24+
DEBUG = True
25+
WTF_CSRF_ENABLED = False
26+
DEBUG_TB_ENABLED = True
27+
28+
29+
class TestingConfig(Config):
30+
TESTING = True
31+
DEBUG = True
32+
SQLALCHEMY_DATABASE_URI = "sqlite:///testdb.sqlite"
33+
BCRYPT_LOG_ROUNDS = 1
34+
WTF_CSRF_ENABLED = False
35+
36+
37+
class ProductionConfig(Config):
38+
DEBUG = False
39+
DEBUG_TB_ENABLED = False

manage.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from flask.cli import FlaskGroup
2+
3+
from src import app
4+
5+
cli = FlaskGroup(app)
6+
7+
8+
if __name__ == "__main__":
9+
cli()

requirements.txt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
alembic==1.12.1
2+
bcrypt==4.0.1
3+
blinker==1.7.0
4+
click==8.1.7
5+
colorama==0.4.6
6+
Flask==3.0.0
7+
Flask-Bcrypt==1.0.1
8+
Flask-Login==0.6.3
9+
Flask-Migrate==4.0.5
10+
Flask-SQLAlchemy==3.1.1
11+
Flask-WTF==1.2.1
12+
greenlet==3.0.1
13+
itsdangerous==2.1.2
14+
Jinja2==3.1.2
15+
Mako==1.3.0
16+
MarkupSafe==2.1.3
17+
pyotp==2.9.0
18+
pypng==0.20220715.0
19+
python-decouple==3.8
20+
qrcode==7.4.2
21+
SQLAlchemy==2.0.23
22+
typing_extensions==4.8.0
23+
Werkzeug==3.0.1
24+
WTForms==3.1.1

src/__init__.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from decouple import config
2+
from flask import Flask
3+
from flask_bcrypt import Bcrypt
4+
from flask_login import LoginManager # Add this line
5+
from flask_migrate import Migrate
6+
from flask_sqlalchemy import SQLAlchemy
7+
8+
app = Flask(__name__)
9+
app.config.from_object(config("APP_SETTINGS"))
10+
11+
bcrypt = Bcrypt(app)
12+
db = SQLAlchemy(app)
13+
migrate = Migrate(app, db)
14+
15+
login_manager = LoginManager() # Add this line
16+
login_manager.init_app(app) # Add this line
17+
18+
# Registering blueprints
19+
from src.accounts.views import accounts_bp
20+
from src.core.views import core_bp
21+
22+
app.register_blueprint(accounts_bp)
23+
app.register_blueprint(core_bp)
24+
25+
from src.accounts.models import User
26+
27+
login_manager.login_view = "accounts.login"
28+
login_manager.login_message_category = "danger"
29+
30+
@login_manager.user_loader
31+
def load_user(user_id):
32+
return User.query.filter(User.id == int(user_id)).first()

src/accounts/__init__.py

Whitespace-only changes.

src/accounts/forms.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from flask_wtf import FlaskForm
2+
from wtforms import StringField, PasswordField
3+
from wtforms.validators import DataRequired, EqualTo, Length, InputRequired
4+
5+
from src.accounts.models import User
6+
7+
8+
class RegisterForm(FlaskForm):
9+
username = StringField(
10+
"Username", validators=[DataRequired(), Length(min=6, max=40)]
11+
)
12+
password = PasswordField(
13+
"Password", validators=[DataRequired(), Length(min=6, max=25)]
14+
)
15+
confirm = PasswordField(
16+
"Repeat password",
17+
validators=[
18+
DataRequired(),
19+
EqualTo("password", message="Passwords must match."),
20+
],
21+
)
22+
23+
def validate(self, extra_validators):
24+
initial_validation = super(RegisterForm, self).validate(extra_validators)
25+
if not initial_validation:
26+
return False
27+
user = User.query.filter_by(username=self.username.data).first()
28+
if user:
29+
self.username.errors.append("Username already registered")
30+
return False
31+
if self.password.data != self.confirm.data:
32+
self.password.errors.append("Passwords must match")
33+
return False
34+
return True
35+
36+
37+
class LoginForm(FlaskForm):
38+
username = StringField("Username", validators=[DataRequired()])
39+
password = PasswordField("Password", validators=[DataRequired()])
40+
41+
42+
class TwoFactorForm(FlaskForm):
43+
otp = StringField('Enter OTP', validators=[
44+
InputRequired(), Length(min=6, max=6)])

src/accounts/models.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from datetime import datetime
2+
3+
import pyotp
4+
from flask_login import UserMixin
5+
6+
from src import bcrypt, db
7+
from config import Config
8+
9+
10+
class User(UserMixin, db.Model):
11+
12+
__tablename__ = "users"
13+
14+
id = db.Column(db.Integer, primary_key=True)
15+
username = db.Column(db.String, unique=True, nullable=False)
16+
password = db.Column(db.String, nullable=False)
17+
created_at = db.Column(db.DateTime, nullable=False)
18+
is_two_factor_authentication_enabled = db.Column(
19+
db.Boolean, nullable=False, default=False)
20+
secret_token = db.Column(db.String, unique=True)
21+
22+
def __init__(self, username, password):
23+
self.username = username
24+
self.password = bcrypt.generate_password_hash(password)
25+
self.created_at = datetime.now()
26+
self.secret_token = pyotp.random_base32()
27+
28+
def get_authentication_setup_uri(self):
29+
return pyotp.totp.TOTP(self.secret_token).provisioning_uri(
30+
name=self.username, issuer_name=Config.APP_NAME)
31+
32+
def is_otp_valid(self, user_otp):
33+
totp = pyotp.parse_uri(self.get_authentication_setup_uri())
34+
return totp.verify(user_otp)
35+
36+
def __repr__(self):
37+
return f"<user {self.username}>"

src/accounts/views.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
from src.utils import get_b64encoded_qr_image
2+
from .forms import LoginForm, RegisterForm, TwoFactorForm
3+
from src.accounts.models import User
4+
from src import db, bcrypt
5+
from flask_login import current_user, login_required, login_user, logout_user
6+
from flask import Blueprint, flash, redirect, render_template, request, url_for
7+
8+
accounts_bp = Blueprint("accounts", __name__)
9+
10+
HOME_URL = "core.home"
11+
SETUP_2FA_URL = "accounts.setup_two_factor_auth"
12+
VERIFY_2FA_URL = "accounts.verify_two_factor_auth"
13+
14+
15+
@accounts_bp.route("/register", methods=["GET", "POST"])
16+
def register():
17+
if current_user.is_authenticated:
18+
if current_user.is_two_factor_authentication_enabled:
19+
flash("You are already registered.", "info")
20+
return redirect(url_for(HOME_URL))
21+
else:
22+
flash(
23+
"You have not enabled 2-Factor Authentication. Please enable first to login.", "info")
24+
return redirect(url_for(SETUP_2FA_URL))
25+
form = RegisterForm(request.form)
26+
if form.validate_on_submit():
27+
try:
28+
user = User(username=form.username.data, password=form.password.data)
29+
db.session.add(user)
30+
db.session.commit()
31+
32+
login_user(user)
33+
flash("You are registered. You have to enable 2-Factor Authentication first to login.", "success")
34+
35+
return redirect(url_for(SETUP_2FA_URL))
36+
except Exception:
37+
db.session.rollback()
38+
flash("Registration failed. Please try again.", "danger")
39+
40+
return render_template("accounts/register.html", form=form)
41+
42+
43+
@accounts_bp.route("/login", methods=["GET", "POST"])
44+
def login():
45+
if current_user.is_authenticated:
46+
if current_user.is_two_factor_authentication_enabled:
47+
flash("You are already logged in.", "info")
48+
return redirect(url_for(HOME_URL))
49+
else:
50+
flash(
51+
"You have not enabled 2-Factor Authentication. Please enable first to login.", "info")
52+
return redirect(url_for(SETUP_2FA_URL))
53+
54+
form = LoginForm(request.form)
55+
if form.validate_on_submit():
56+
user = User.query.filter_by(username=form.username.data).first()
57+
if user and bcrypt.check_password_hash(user.password, request.form["password"]):
58+
login_user(user)
59+
if not current_user.is_two_factor_authentication_enabled:
60+
flash(
61+
"You have not enabled 2-Factor Authentication. Please enable first to login.", "info")
62+
return redirect(url_for(SETUP_2FA_URL))
63+
return redirect(url_for(VERIFY_2FA_URL))
64+
elif not user:
65+
flash("You are not registered. Please register.", "danger")
66+
else:
67+
flash("Invalid username and/or password.", "danger")
68+
return render_template("accounts/login.html", form=form)
69+
70+
71+
@accounts_bp.route("/logout")
72+
@login_required
73+
def logout():
74+
logout_user()
75+
flash("You were logged out.", "success")
76+
return redirect(url_for("accounts.login"))
77+
78+
79+
@accounts_bp.route("/setup-2fa")
80+
@login_required
81+
def setup_two_factor_auth():
82+
secret = current_user.secret_token
83+
uri = current_user.get_authentication_setup_uri()
84+
base64_qr_image = get_b64encoded_qr_image(uri)
85+
return render_template("accounts/setup-2fa.html", secret=secret, qr_image=base64_qr_image)
86+
87+
88+
@accounts_bp.route("/verify-2fa", methods=["GET", "POST"])
89+
@login_required
90+
def verify_two_factor_auth():
91+
form = TwoFactorForm(request.form)
92+
if form.validate_on_submit():
93+
if current_user.is_otp_valid(form.otp.data):
94+
if current_user.is_two_factor_authentication_enabled:
95+
flash("2FA verification successful. You are logged in!", "success")
96+
return redirect(url_for(HOME_URL))
97+
else:
98+
try:
99+
current_user.is_two_factor_authentication_enabled = True
100+
db.session.commit()
101+
flash("2FA setup successful. You are logged in!", "success")
102+
return redirect(url_for(HOME_URL))
103+
except Exception:
104+
db.session.rollback()
105+
flash("2FA setup failed. Please try again.", "danger")
106+
return redirect(url_for(VERIFY_2FA_URL))
107+
else:
108+
flash("Invalid OTP. Please try again.", "danger")
109+
return redirect(url_for(VERIFY_2FA_URL))
110+
else:
111+
if not current_user.is_two_factor_authentication_enabled:
112+
flash(
113+
"You have not enabled 2-Factor Authentication. Please enable it first.", "info")
114+
return render_template("accounts/verify-2fa.html", form=form)

src/core/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)