diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..ad512eed6db3ea1f012ac3b4eb3b7bea80b505ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__ + +# Database +data.db + +# Virtualenv +bam-venv diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000000000000000000000000000000000..a3751e321dc608271925968ad1a5dbeade88e3c6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "python.formatting.provider": "black", + "python.formatting.blackPath": "black", + "editor.formatOnSave": true, + "[python]": { + "editor.defaultFormatter": null + } +} diff --git a/bam/__init__.py b/bam/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2ac4998f38cc3855f4e3aa7f86b9db30958882a9 --- /dev/null +++ b/bam/__init__.py @@ -0,0 +1,15 @@ +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from flask_bcrypt import Bcrypt +from flask_login import LoginManager + +app = Flask(__name__) +app.config["SECRET_KEY"] = "enterasecretkeyhere" +app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///data.db" +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + +db = SQLAlchemy(app) +bcrypt = Bcrypt(app) +login_manager = LoginManager(app) + +from bam import routes diff --git a/bam/forms.py b/bam/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..58352643ce0c29c9b777bff3dda840bae919de30 --- /dev/null +++ b/bam/forms.py @@ -0,0 +1,26 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField, SubmitField, BooleanField +from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError +from bam.models import User + + +class RegistrationForm(FlaskForm): + email = StringField("Email", validators=[DataRequired(), Email()], render_kw={"placeholder": "Enter your email"}) + password = PasswordField("Password", validators=[DataRequired()], render_kw={"placeholder": "Choose a strong password"}) + confirm_password = PasswordField( + "Confirm Password", validators=[DataRequired(), EqualTo("password")], + render_kw={"placeholder":"Re-enter your password"} + ) + + submit = SubmitField("Sign Up") + + def validate_email(self, email): + if User.query.filter_by(email=email.data).first(): + raise ValidationError("A user with this email already exists.") + + +class LoginForm(FlaskForm): + email = StringField("Email", validators=[DataRequired(), Email()], render_kw={"placeholder": "bookmaster@bam.com"}) + password = PasswordField("Password", validators=[DataRequired()], render_kw={"placeholder": "masterofbooks"}) + remember = BooleanField("Remember Me") + submit = SubmitField("Login") diff --git a/bam/models.py b/bam/models.py new file mode 100644 index 0000000000000000000000000000000000000000..8f593fac5de87785d8fc02e4aa02ef8554d50915 --- /dev/null +++ b/bam/models.py @@ -0,0 +1,16 @@ +from bam import db, login_manager +from flask_login import UserMixin + + +@login_manager.user_loader +def load_user(user_id): + return User.query.get(int(user_id)) + + +class User(db.Model, UserMixin): + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(120), unique=True, nullable=False) + password = db.Column(db.String(60), nullable=False) + + def __repr__(self): + return f"User({self.email}, {self.password})" diff --git a/bam/routes.py b/bam/routes.py new file mode 100644 index 0000000000000000000000000000000000000000..b4ef8ff7d07680aaac5e84dadfc85036afe7e980 --- /dev/null +++ b/bam/routes.py @@ -0,0 +1,53 @@ +from flask import render_template, url_for, flash, redirect +from flask_login import login_user, current_user, logout_user +from bam import app, db, bcrypt +from bam.forms import RegistrationForm, LoginForm +from bam.models import User + + +@app.route("/") +@app.route("/home") +def home(): + if current_user.is_authenticated: + return render_template("dash.html") + return render_template("home.html") + + +@app.route("/register", methods=["GET", "POST"]) +def register(): + if current_user.is_authenticated: + return redirect(url_for("home")) + form = RegistrationForm() + if form.validate_on_submit(): + hashed_password = bcrypt.generate_password_hash(form.password.data).decode( + "utf-8" + ) + user = User(email=form.email.data, password=hashed_password) + db.session.add(user) + db.session.commit() + flash( + f"Account successfully created! You may login now.", "success" + ) + return redirect(url_for("login")) + return render_template("register.html", title="Register", form=form) + + +@app.route("/login", methods=["GET", "POST"]) +def login(): + if current_user.is_authenticated: + return redirect(url_for("home")) + form = LoginForm() + if form.validate_on_submit(): + user = User.query.filter_by(email=form.email.data).first() + if user and bcrypt.check_password_hash(user.password, form.password.data): + login_user(user, remember=form.remember.data) + return redirect(url_for("home")) + flash("Hmmm .. those credentials don't seem to be right", "error") + return render_template("login.html", title="Login", form=form) + + +@app.route("/logout") +def logout(): + logout_user() + # flash("You have been successfully logged out", "success") + return redirect(url_for("home")) \ No newline at end of file diff --git a/bam/static/home.css b/bam/static/home.css new file mode 100644 index 0000000000000000000000000000000000000000..f5fb381f347bd9049f0468b6f52e242b2eeeff61 --- /dev/null +++ b/bam/static/home.css @@ -0,0 +1,38 @@ +.tagline { + font-size: 3.5rem; + text-align: center; + font-family: Roboto; + font-weight: 700; + text-shadow: 1px 1px 5px rgba(0, 0, 0, 0.6); +} + +.explanation { + text-align: center; + font-size: 1.5rem; + text-shadow: 1px 1px 5px rgba(0, 0, 0, 0.6); +} + +.jk { + font-size: 0.7rem; + color: rgba(0, 0, 0, 0.1); +} + +@media screen and (max-width: 600px) { + .tagline { + font-size: 2.5rem; + margin-bottom: 10px; + } + .explanation { + font-size: 1.3rem; + } +} + +@media screen and (min-width: 1200px) { + .tagline { + font-size: 4rem; + margin-bottom: 10px; + } + .explanation { + font-size: 1.6rem; + } +} diff --git a/bam/static/outform.css b/bam/static/outform.css new file mode 100644 index 0000000000000000000000000000000000000000..c6d6ad972f1e282476a23a20964afb2febb16c71 --- /dev/null +++ b/bam/static/outform.css @@ -0,0 +1,109 @@ +.container { + background-color: var(--t-dark); + padding: 15px; + border-radius: 3px; +} + +.f-message { + font-style: oblique; +} + +h1 { + text-align: center; +} + +form { + display: flex; + flex-direction: column; + gap: 5px; + min-width: min(90vw, 400px); +} + +input { + font-family: Roboto; + font-size: 16px; + /* margin-bottom: 5px; */ + border-radius: 3px; + padding: 17px; + background-color: var(--t-dark); + border: 1px solid black; + color: #eee; + transition: background 0.3s; + /* margin: 3px 0 10px; */ +} +.field:hover { + background-color: rgba(0, 0, 0, 0.9); +} +.field:focus, +.field:active { + background-color: rgba(30, 30, 30, 0.5); + border: 1px solid grey; +} + +label { + text-transform: uppercase; + font-weight: 700; + font-size: 0.9rem; + letter-spacing: 1px; +} + +.field-error { + font-style: oblique; + color: var(--red); + font-size: 0.9rem; +} + +[type="submit"] { + background-color: var(--red); + border: none; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1px; +} + +[type="submit"]:hover { + background-color: var(--dark-red); + cursor: pointer; +} + +label, +[type="submit"] { + margin-top: 10px; +} + +/* Checkbox - inspired by https://codepen.io/grayghostvisuals/pen/yOxGxz */ +.remember-row { + display: flex; + align-items: center; + gap: 15px; + margin-top: 10px; +} + +.remember-row input { + order: 2; + opacity: 0; +} +.checkbox-indicator { + display: flex; + height: 1.5rem; + width: 1.5rem; + border: 1px solid black; + align-items: center; + transition: border 0.2s ease; + background-color: var(--t-dark); +} + +.checkbox-indicator svg { + height: 50%; + width: 100%; + fill: transparent; +} + +.remember-row:hover .checkbox-indicator { + border: 1px solid grey; + background-color: rgba(30, 30, 30, 0.5); +} + +.remember-input:checked + .checkbox-indicator svg { + fill: var(--red); +} diff --git a/bam/static/outlayout.css b/bam/static/outlayout.css new file mode 100644 index 0000000000000000000000000000000000000000..3edf17fb34738c7c585be65ad1423fb8e5485cb5 --- /dev/null +++ b/bam/static/outlayout.css @@ -0,0 +1,100 @@ +@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500;700;900&display=swap"); +html, +body { + margin: 0; + padding: 0; + min-height: 100vh; +} + +:root { + --red: #e50914; + --dark-red: #bb0009; + --t-dark: rgba(0, 0, 0, 0.8); +} + +* { + box-sizing: border-box; +} + +body { + background-image: url("https://i.imgur.com/4LLrhbS.jpg"); + background-position: center; + background-repeat: no-repeat; + background-size: cover; + color: white; + font-family: Roboto; +} + +.maincontainer { + display: flex; + flex-direction: column; + align-items: center; + min-height: 100vh; +} + +.navbar { + height: 100px; + align-items: center; + padding: 20px 50px; + display: flex; + width: 100%; + gap: 10px; +} + +.navbar .logo { + height: 100%; +} + +.navbar .logo img { + height: 100%; + filter: drop-shadow(2px 2px 6px #000); +} + +.navbar .space { + flex-grow: 1; +} + +.navbar button { + background-color: var(--red); + border: none; + color: white; + padding: 5px 10px; + font-family: Roboto; + border-radius: 2px; + font-size: 1.1rem; + box-shadow: 1px 1px 5px black; + transition: background 0.3s; + cursor: pointer; +} + +.navbar button:hover { + background-color: var(--dark-red); +} + +.content { + flex-grow: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 65ch; + max-width: 90vw; +} + +.rowspacer { + height: 10vh; +} + +.credits { + padding: 15px; + font-size: 0.7rem; + color: #460e0c; + opacity: 0.3; +} + +@media screen and (max-width: 600px) { + .navbar { + padding: 15px; + height: 76px; + } +} diff --git a/bam/templates/dash.html b/bam/templates/dash.html new file mode 100644 index 0000000000000000000000000000000000000000..439b8cc766edea2ed31d06659c115492fb0758c3 --- /dev/null +++ b/bam/templates/dash.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>BAM - Dashboard</title> + </head> + <body> + Yaaay - you are authorized! + </body> +</html> diff --git a/bam/templates/home.html b/bam/templates/home.html new file mode 100644 index 0000000000000000000000000000000000000000..144154929581aea9d8044daac0978a123cae2ab3 --- /dev/null +++ b/bam/templates/home.html @@ -0,0 +1,12 @@ +{% extends "outlayout.html" %} {% block head %} +<link rel="stylesheet" href="{{ url_for('static', filename='home.css') }}" /> +{% endblock head %} {% block content %} +<div class="tagline">The only missing piece in your life.</div> +<div class="explanation"> + BAM is an all-in-one Book Management System built on the principles of + simplicity and minimalism +</div> +<div class="jk"> + Nah, just kidding, it wasn't built with anything in mind lol +</div> +{% endblock content %} diff --git a/bam/templates/login.html b/bam/templates/login.html new file mode 100644 index 0000000000000000000000000000000000000000..6c7e0ef7807a1bb67c1d626ac2374dac2dba624e --- /dev/null +++ b/bam/templates/login.html @@ -0,0 +1,39 @@ +{% extends "outlayout.html" %} {% block head %} +<link rel="stylesheet" href="{{ url_for('static', filename='outform.css') }}" /> +{% endblock head %} {% block content %} +<div class="container"> + <form action="" method="POST"> + {{ form.hidden_tag() }} + <h1>Sign In</h1> + {% with messages = get_flashed_messages(with_categories=True) %} {% if + messages %} {% for category, message in messages %} + <div class="f-message f-message-{{ category }}">{{ message }}</div> + {% endfor %} {% endif %} {% endwith %} + <!-- <div>Your account has been successfully created.</div> --> + {{ form.email.label() }} {{ form.email(class="field") }} {% for error in + form.email.errors %} + <div class="field-error">{{ error }}</div> + {% endfor %} {{ form.password.label() }} {{ form.password(class="field") }} + {% for error in form.password.errors %} + <div class="field-error">{{ error }}</div> + {% endfor %} + <label for="{{ form.remember.label.field_id }}" class="remember-row"> + {{ form.remember(class="remember-input") }} + <span class="checkbox-indicator" aria-hidden="true"> + <svg + version="1.1" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + viewBox="0 0 21 16" + > + <path + d="M21,2.1L7.3,16l-0.4-0.4l-0.3,0.3L0,9.3l2.1-2.1l4.9,4.9L18.9,0L21,2.1z" + /> + </svg> + </span> + <div>{{ form.remember.label.text }}</div> + </label> + {{ form.submit() }} + </form> +</div> +{% endblock content %} diff --git a/bam/templates/outlayout.html b/bam/templates/outlayout.html new file mode 100644 index 0000000000000000000000000000000000000000..99a5aa7cffbeac91a620b0f4e35ed22982f4cc8b --- /dev/null +++ b/bam/templates/outlayout.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + {% if title %} + <title>BAM - {{ title }}</title> + {% else %} + <title>BAM</title> + {% endif %} + <link + rel="stylesheet" + href="{{ url_for('static', filename='outlayout.css') }}" + /> + {% block head %}{% endblock %} + </head> + <body> + <div class="maincontainer"> + <div class="navbar"> + <div class="logo"> + <a href="{{ url_for('home') }}"> + <img src="https://i.imgur.com/t2ZlJ06.png" /> + </a> + </div> + <div class="space"></div> + <a href="{{ url_for('login') }}"><button>Sign In</button></a> + <a href="{{ url_for('register') }}"><button>Register</button></a> + </div> + <div class="content">{% block content %}{% endblock %}</div> + <div class="rowspacer"></div> + <div class="credits">Built with ❤ using Flask</div> + </div> + </body> +</html> diff --git a/bam/templates/register.html b/bam/templates/register.html new file mode 100644 index 0000000000000000000000000000000000000000..b66522c09c2d72626d9b6dbbfe69109554b102f5 --- /dev/null +++ b/bam/templates/register.html @@ -0,0 +1,22 @@ +{% extends "outlayout.html" %} {% block head %} +<link rel="stylesheet" href="{{ url_for('static', filename='outform.css') }}" /> +{% endblock head %} {% block content %} +<div class="container"> + <form action="" method="POST"> + {{ form.hidden_tag() }} + <h1>Create an account</h1> + {{ form.email.label() }} {{ form.email(class="field") }} {% for error in + form.email.errors %} + <div class="field-error">{{ error }}</div> + {% endfor %} {{ form.password.label() }} {{ form.password(class="field") }} + {% for error in form.password.errors %} + <div class="field-error">{{ error }}</div> + {% endfor %} {{ form.confirm_password.label() }} {{ + form.confirm_password(class="field") }} {% for error in + form.confirm_password.errors %} + <div class="field-error">{{ error }}</div> + {% endfor %} + <input type="submit" value="Sign in" /> + </form> +</div> +{% endblock content %} diff --git a/create_db.py b/create_db.py new file mode 100644 index 0000000000000000000000000000000000000000..e6f7d5eb03e48a182b6b0eb43cf7731af9d02ba8 --- /dev/null +++ b/create_db.py @@ -0,0 +1,3 @@ +from bam import db + +db.create_all() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..1752fdb71ae630dc609b565728f55259ab4ee559 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,27 @@ +appdirs==1.4.4 +bcrypt==3.2.0 +black==20.8b1 +cffi==1.14.4 +click==7.1.2 +dnspython==2.0.0 +email-validator==1.1.2 +Flask==1.1.2 +Flask-Bcrypt==0.7.1 +Flask-Login==0.5.0 +Flask-SQLAlchemy==2.4.4 +Flask-WTF==0.14.3 +idna==2.10 +itsdangerous==1.1.0 +Jinja2==2.11.2 +MarkupSafe==1.1.1 +mypy-extensions==0.4.3 +pathspec==0.8.1 +pycparser==2.20 +regex==2020.11.13 +six==1.15.0 +SQLAlchemy==1.3.22 +toml==0.10.2 +typed-ast==1.4.1 +typing-extensions==3.7.4.3 +Werkzeug==1.0.1 +WTForms==2.3.3 diff --git a/run.py b/run.py new file mode 100644 index 0000000000000000000000000000000000000000..83f079b62e1fd7ba4b0ec30e7c1ff3b08c0ecec3 --- /dev/null +++ b/run.py @@ -0,0 +1,4 @@ +from bam import app + +if __name__ == "__main__": + app.run(debug=True)