commit 00314f155a0888ff16778e89a9647da02d6b1c78 Author: dogeystamp Date: Thu Mar 9 16:38:57 2023 -0500 initial commit by this point we have: - flask boilerplate - sqlite - user creation/deletion cli diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..00e5b19 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +venv +__pycache__/ +.env +config.yml +instance/ diff --git a/config.yml.example b/config.yml.example new file mode 100644 index 0000000..a33b081 --- /dev/null +++ b/config.yml.example @@ -0,0 +1,8 @@ +# vim: filetype=yaml + +secret_key: "" + +sqlalchemy_basedir: "sqlite:///" +sqlalchemy_basename: "sachet" + +# bcrypt_log_rounds: 13 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0534176 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,16 @@ +bcrypt==4.0.1 +click==8.1.3 +Flask==2.2.3 +Flask-Bcrypt==1.0.1 +Flask-Cors==3.0.10 +Flask-Script==2.0.6 +Flask-SQLAlchemy==3.0.3 +greenlet==2.0.2 +itsdangerous==2.1.2 +Jinja2==3.1.2 +MarkupSafe==2.1.2 +PyYAML==6.0 +six==1.16.0 +SQLAlchemy==2.0.5.post1 +typing_extensions==4.5.0 +Werkzeug==2.2.3 diff --git a/sachet/__init__.py b/sachet/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sachet/server/__init__.py b/sachet/server/__init__.py new file mode 100644 index 0000000..ed10043 --- /dev/null +++ b/sachet/server/__init__.py @@ -0,0 +1,22 @@ +import os +from flask import Flask +from flask_cors import CORS +from flask_sqlalchemy import SQLAlchemy +from flask_bcrypt import Bcrypt + +app = Flask(__name__) +CORS(app) + +app_settings = os.getenv( + 'APP_SETTINGS', + 'sachet.server.config.DevelopmentConfig' +) +app.config.from_object(app_settings) + +bcrypt = Bcrypt(app) +db = SQLAlchemy(app) + +import sachet.server.commands + +from sachet.server.auth.views import auth_blueprint +app.register_blueprint(auth_blueprint) diff --git a/sachet/server/auth/__init__.py b/sachet/server/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sachet/server/auth/views.py b/sachet/server/auth/views.py new file mode 100644 index 0000000..4cafcb1 --- /dev/null +++ b/sachet/server/auth/views.py @@ -0,0 +1,7 @@ +from flask import Blueprint + +auth_blueprint = Blueprint("auth_blueprint", __name__) + +@auth_blueprint.route('/') +def index(): + return "Hello world" diff --git a/sachet/server/commands.py b/sachet/server/commands.py new file mode 100644 index 0000000..8fc9863 --- /dev/null +++ b/sachet/server/commands.py @@ -0,0 +1,65 @@ +import click +from sachet.server import app, db +from sachet.server.models import User +from flask.cli import AppGroup + + +db_cli = AppGroup("db") + +@db_cli.command("create") +def create_db(): + """Create all db tables.""" + db.create_all() + + +@db_cli.command("drop") +@click.option('--yes', is_flag=True, expose_value=False, prompt="Are you sure you want to drop all tables?") +def drop_db(): + """Drop all db tables.""" + db.drop_all() + +app.cli.add_command(db_cli) + + +user_cli = AppGroup("user") + +@user_cli.command("create") +@click.option("--admin", default=False, prompt="Set this user as administrator?", help="Set this user an administrator.") +@click.option("--username", prompt="Username", help="Sets the username.") +@click.option("--password", + prompt="Password", + hide_input=True, + help="Sets the user's password (for security, avoid setting this from the command line).") +def create_user(admin, username, password): + """Create a user directly in the database.""" + user = User.query.filter_by(username=username).first() + if not user: + user = User( + username=username, + password=password, + admin=admin + ) + db.session.add(user) + db.session.commit() + else: + raise Exception(f"User '{username}' already exists (ID: {user.id})") + +@user_cli.command("delete") +@click.option("--username", "selector", help="Delete by username.", flag_value="username", default=True) +@click.option("--id", "selector", help="Delete by ID.", flag_value="ID", default=True) +@click.argument("usersel") +@click.option('--yes', is_flag=True, expose_value=False, prompt=f"Are you sure you want to delete this user?") +def delete_user(selector, usersel): + if selector == "username": + user = User.query.filter_by(username=usersel).first() + elif selector == "ID": + user = User.query.filter_by(id=usersel).first() + + if not user: + raise KeyError(f"User ({selector} {usersel}) does not exist.") + else: + db.session.delete(user) + db.session.commit() + + +app.cli.add_command(user_cli) diff --git a/sachet/server/config.py b/sachet/server/config.py new file mode 100644 index 0000000..b215d5b --- /dev/null +++ b/sachet/server/config.py @@ -0,0 +1,39 @@ +from os import environ, path +import yaml + +config_locations = ["/etc/sachet", "."] +config_path = "" + +for dir in config_locations: + file_path = f"{dir}/config.yml" + if path.exists(file_path): + config_path = file_path + break + +if config_path == "": + raise FileNotFoundError("Please create a configuration: copy config.yml.example to config.yml.") + +config = yaml.safe_load(open(config_path)) + +if config["secret_key"] == "": + raise ValueError("Please set secret_key within the configuration.") + +class BaseConfig: + DEBUG = False + SQLALCHEMY_DATABASE_URI = config["sqlalchemy_basedir"] + config["sqlalchemy_basename"] + ".db" + SECRET_KEY = config["secret_key"] + BCRYPT_LOG_ROUNDS = config.get("bcrypt_log_rounds", 13) + SQLALCHEMY_TRACK_MODIFICATIONS = False + +class TestingConfig(BaseConfig): + DEBUG = True + SQLALCHEMY_DATABASE_URI = config["sqlalchemy_basedir"] + config["sqlalchemy_basename"] + "_test" + ".db" + BCRYPT_LOG_ROUNDS = config.get("bcrypt_log_rounds", 4) + +class DevelopmentConfig(BaseConfig): + DEBUG = True + SQLALCHEMY_DATABASE_URI = config["sqlalchemy_basedir"] + config["sqlalchemy_basename"] + "_dev" + ".db" + BCRYPT_LOG_ROUNDS = config.get("bcrypt_log_rounds", 4) + +class ProductionConfig(BaseConfig): + DEBUG = False diff --git a/sachet/server/models.py b/sachet/server/models.py new file mode 100644 index 0000000..b10c5b3 --- /dev/null +++ b/sachet/server/models.py @@ -0,0 +1,19 @@ +from sachet.server import app, db, bcrypt +import datetime + +class User(db.Model): + __tablename__ = "users" + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + username = db.Column(db.String(255), unique=True, nullable=False) + password = db.Column(db.String(255), nullable=False) + register_date = db.Column(db.DateTime, nullable=False) + admin = db.Column(db.Boolean, nullable=False, default=False) + + def __init__(self, username, password, admin=False): + self.password = bcrypt.generate_password_hash( + password, app.config.get("BCRYPT_LOG_ROUNDS") + ).decode() + self.username = username + self.register_date = datetime.datetime.now() + self.admin = admin