lombik 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. lombik-0.1.0/LICENSE +22 -0
  2. lombik-0.1.0/PKG-INFO +53 -0
  3. lombik-0.1.0/README.md +35 -0
  4. lombik-0.1.0/lombik/__init__.py +0 -0
  5. lombik-0.1.0/lombik/cli.py +64 -0
  6. lombik-0.1.0/lombik/templates/__init__.py +0 -0
  7. lombik-0.1.0/lombik/templates/createapp/app.py +240 -0
  8. lombik-0.1.0/lombik/templates/createapp/blueprints/auth/models.py +71 -0
  9. lombik-0.1.0/lombik/templates/createapp/blueprints/auth/roles.py +4 -0
  10. lombik-0.1.0/lombik/templates/createapp/blueprints/auth/routes.py +41 -0
  11. lombik-0.1.0/lombik/templates/createapp/blueprints/auth/services.py +162 -0
  12. lombik-0.1.0/lombik/templates/createapp/blueprints/core/error_logging.py +27 -0
  13. lombik-0.1.0/lombik/templates/createapp/blueprints/core/models.py +21 -0
  14. lombik-0.1.0/lombik/templates/createapp/blueprints/core/routes.py +19 -0
  15. lombik-0.1.0/lombik/templates/createapp/blueprints/settings/modesl.py +0 -0
  16. lombik-0.1.0/lombik/templates/createapp/blueprints/settings/routes.py +19 -0
  17. lombik-0.1.0/lombik/templates/createapp/blueprints/settings/services.py +0 -0
  18. lombik-0.1.0/lombik/templates/createapp/config.py +53 -0
  19. lombik-0.1.0/lombik/templates/createapp/db.py +5 -0
  20. lombik-0.1.0/lombik/templates/createapp/models/__init__.py +11 -0
  21. lombik-0.1.0/lombik/templates/createapp/tools.py +45 -0
  22. lombik-0.1.0/lombik/templates/createapp/wrappers.py +13 -0
  23. lombik-0.1.0/lombik.egg-info/PKG-INFO +53 -0
  24. lombik-0.1.0/lombik.egg-info/SOURCES.txt +28 -0
  25. lombik-0.1.0/lombik.egg-info/dependency_links.txt +1 -0
  26. lombik-0.1.0/lombik.egg-info/entry_points.txt +2 -0
  27. lombik-0.1.0/lombik.egg-info/requires.txt +9 -0
  28. lombik-0.1.0/lombik.egg-info/top_level.txt +1 -0
  29. lombik-0.1.0/pyproject.toml +27 -0
  30. lombik-0.1.0/setup.cfg +4 -0
lombik-0.1.0/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 David Plajner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
lombik-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.4
2
+ Name: lombik
3
+ Version: 0.1.0
4
+ Summary: Flask scaffold engine
5
+ Author: David Plajner
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: Flask
9
+ Requires-Dist: Flask-SQLAlchemy
10
+ Requires-Dist: Flask-Migrate
11
+ Requires-Dist: Flask-WTF
12
+ Requires-Dist: Flask-Session
13
+ Requires-Dist: python-dotenv
14
+ Requires-Dist: click
15
+ Requires-Dist: SQLAlchemy
16
+ Requires-Dist: Werkzeug
17
+ Dynamic: license-file
18
+
19
+ # Lombik
20
+ #### A scaffold engine for Flask, the greatest framework of all times.
21
+
22
+ This readme will serve perfectly as a self documentation file as I go, and at the end of it I will re-write it with the final details.
23
+ That means that if you see this, either my company cleersoftware has taken off and you might work with me on enhancing the scaffold for our apps.
24
+ Another possibility is that lombik took off, and you are reading the early commits. Have fun, there is probably a lot of them.
25
+ Anyways, I'm gonan actually start on explaning / plannign how thigns will work.
26
+
27
+ > Install lombik
28
+ Run `pip install lombik`
29
+
30
+ > Create app
31
+ To create an app called *myapp*, run `lombik createapp myapp` then `cd myapp`
32
+ This creates the entire structure of your application in one go and you can run `flask run --debug`
33
+
34
+ > Create superuser
35
+ Lombik comes with MySQL as a default db. In the generated .env file you'll find the credentials for local development.
36
+ Replace those placeholders with your actual dev credentials and production if you have it already.
37
+ When done, initialize the database by running `flask initdb` and then you can run `flask superuser` to create your owner account.
38
+ With this, you'll be able to log in to your app and start developing from there.
39
+
40
+ This is a quite simple auth system, in fact it is simple by design and meant to be replaced for production apps with something more robust.
41
+
42
+ > Features and benefits
43
+ On top of the structural guidance, lombik comes with a few built in goodies.
44
+
45
+ - User authentication and session handling
46
+ - CSRF / session expiry
47
+ - Error handling
48
+ - Structure
49
+ - Template filters for frontend development
50
+ - Pre-imported libraries
51
+ - General templates for desktop and mobile
52
+
53
+
lombik-0.1.0/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # Lombik
2
+ #### A scaffold engine for Flask, the greatest framework of all times.
3
+
4
+ This readme will serve perfectly as a self documentation file as I go, and at the end of it I will re-write it with the final details.
5
+ That means that if you see this, either my company cleersoftware has taken off and you might work with me on enhancing the scaffold for our apps.
6
+ Another possibility is that lombik took off, and you are reading the early commits. Have fun, there is probably a lot of them.
7
+ Anyways, I'm gonan actually start on explaning / plannign how thigns will work.
8
+
9
+ > Install lombik
10
+ Run `pip install lombik`
11
+
12
+ > Create app
13
+ To create an app called *myapp*, run `lombik createapp myapp` then `cd myapp`
14
+ This creates the entire structure of your application in one go and you can run `flask run --debug`
15
+
16
+ > Create superuser
17
+ Lombik comes with MySQL as a default db. In the generated .env file you'll find the credentials for local development.
18
+ Replace those placeholders with your actual dev credentials and production if you have it already.
19
+ When done, initialize the database by running `flask initdb` and then you can run `flask superuser` to create your owner account.
20
+ With this, you'll be able to log in to your app and start developing from there.
21
+
22
+ This is a quite simple auth system, in fact it is simple by design and meant to be replaced for production apps with something more robust.
23
+
24
+ > Features and benefits
25
+ On top of the structural guidance, lombik comes with a few built in goodies.
26
+
27
+ - User authentication and session handling
28
+ - CSRF / session expiry
29
+ - Error handling
30
+ - Structure
31
+ - Template filters for frontend development
32
+ - Pre-imported libraries
33
+ - General templates for desktop and mobile
34
+
35
+
File without changes
@@ -0,0 +1,64 @@
1
+ from getpass import getpass
2
+ from pathlib import Path
3
+ import secrets
4
+ import shutil
5
+ import click
6
+
7
+
8
+ BASE_DIR = Path(__file__).parent
9
+
10
+ STARTUP_TEMPLATE = BASE_DIR / "templates" / "createapp"
11
+
12
+
13
+ @click.group()
14
+ def cli():
15
+ pass
16
+
17
+ def is_text_file(path: Path) -> bool:
18
+ try:
19
+ path.read_text(encoding="utf-8")
20
+ return True
21
+ except UnicodeDecodeError:
22
+ return False
23
+ except Exception:
24
+ return False
25
+
26
+
27
+ def replace_placeholders(target_dir, replacements):
28
+
29
+ for file in Path(target_dir).rglob("*"):
30
+
31
+ if not file.is_file():
32
+ continue
33
+
34
+ try:
35
+ content = file.read_text(encoding="utf-8")
36
+ except UnicodeDecodeError:
37
+ continue
38
+
39
+ for key, value in replacements.items():
40
+ content = content.replace(key, value)
41
+
42
+ file.write_text(content, encoding="utf-8")
43
+
44
+
45
+ @cli.command()
46
+ @click.argument("name")
47
+ def createapp(name):
48
+
49
+ target = Path.cwd() / name
50
+
51
+ replacements = {
52
+ "{{SECRET_KEY}}": secrets.token_urlsafe(64),
53
+ "{{CRKEY}}": secrets.token_urlsafe(64)
54
+ }
55
+
56
+ shutil.copytree(STARTUP_TEMPLATE, target)
57
+
58
+ replace_placeholders(target, replacements)
59
+
60
+ print(f"Created app: {name}")
61
+
62
+
63
+ if __name__ == "__main__":
64
+ cli()
File without changes
@@ -0,0 +1,240 @@
1
+ from flask import Flask, g, session, render_template, send_from_directory, current_app
2
+ from flask.cli import with_appcontext
3
+ from flask_wtf.csrf import CSRFError
4
+ from flask_wtf import CSRFProtect
5
+ from flask_session import Session
6
+ from dotenv import load_dotenv
7
+ from zoneinfo import ZoneInfo
8
+ import time
9
+ import os
10
+
11
+ from db import db, migrate
12
+
13
+ from config import config_dict
14
+ from models import load_models
15
+
16
+ # import all your bleurpints here
17
+ from blueprints.core.routes import core_bp
18
+ from blueprints.auth.routes import auth_bp
19
+ from blueprints.settings.routes import settings_bp
20
+
21
+ load_dotenv()
22
+
23
+
24
+ def create_app(env="default"):
25
+ app = Flask(__name__, subdomain_matching=False)
26
+
27
+ load_models()
28
+ # load models first otherwise fetch will never happen
29
+ # also, stop trying to make fetch happen
30
+ _user_management(app)
31
+
32
+ _init_config(app, env)
33
+ _init_extensions(app)
34
+ _init_session_dir(app)
35
+ _init_filters(app)
36
+ _init_blueprints(app)
37
+ _init_hooks(app)
38
+ _init_routes(app)
39
+ _init_error_handlers(app)
40
+
41
+ return app
42
+
43
+
44
+ def _init_config(app, env):
45
+ cfg = config_dict[env]()
46
+
47
+ app.config.from_object(cfg)
48
+ app.config.update(
49
+ SECRET_KEY=cfg.SECRET_KEY,
50
+ CACHE_TYPE=cfg.CACHE_TYPE,
51
+ CACHE_DEFAULT_TIMEOUT=int(cfg.CACHE_DEFAULT_TIMEOUT),
52
+ )
53
+
54
+ def _init_extensions(app):
55
+ Session(app)
56
+ CSRFProtect(app)
57
+ db.init_app(app)
58
+ migrate.init_app(app, db)
59
+
60
+
61
+ def _init_session_dir(app):
62
+ if app.config.get("SESSION_TYPE") == "filesystem":
63
+ os.makedirs(app.config["SESSION_FILE_DIR"], exist_ok=True)
64
+
65
+
66
+ def _init_blueprints(app):
67
+ app.register_blueprint(core_bp, url_prefix="/")
68
+ app.register_blueprint(auth_bp, url_prefix="/auth")
69
+ app.register_blueprint(settings_bp, url_prefix="/settings")
70
+
71
+
72
+ def _init_hooks(app):
73
+ #app.before_request(load_user)
74
+ app.before_request(_csrf_lifetime_tracker)
75
+
76
+
77
+ def _csrf_lifetime_tracker():
78
+ if "csrf_last_reset" not in session:
79
+ session["csrf_last_reset"] = int(time.time())
80
+
81
+ session["csrf_last_reset"] = int(time.time())
82
+
83
+ g.csrf_time_left = current_app.config.get("WTF_CSRF_TIME_LIMIT", 3600)
84
+
85
+
86
+ """
87
+ These are all the custom templating commands you can use with lombik.
88
+
89
+ As an example, timestamps in the db are stored as UTC.
90
+ By default, lombik will create the user in the g object with the user's timezone
91
+
92
+ Now to display the UTC timestamp to the user's local timestamp all you have to do is:
93
+
94
+ {{ created_at | localtime }}
95
+
96
+ And it convert UTC to user's local.
97
+
98
+ You can also do dateonly, which by default converts it to UTC and strips out the date.
99
+ There is more.
100
+
101
+ """
102
+ def _init_filters(app):
103
+ from markdown import markdown
104
+
105
+ @app.template_filter("localtimezone")
106
+ def localtimezone(dt):
107
+ if not dt:
108
+ return ""
109
+
110
+ tz = "UTC"
111
+ if hasattr(g, "user") and g.tenant:
112
+ tz = g.tenant.timezone
113
+
114
+ return dt.astimezone(ZoneInfo(tz))
115
+
116
+
117
+ def _fmt(dt, fmt):
118
+ dt = localtimezone(dt)
119
+ return dt.strftime(fmt) if dt else ""
120
+
121
+
122
+ @app.template_filter("onlydate")
123
+ def onlydate(dt):
124
+ return _fmt(dt, "%Y-%m-%d")
125
+
126
+
127
+ @app.template_filter("onlytime")
128
+ def onlytime(dt):
129
+ return _fmt(dt, "%H:%M")
130
+
131
+
132
+ @app.template_filter("localtime")
133
+ def localtime(dt):
134
+ return _fmt(dt, "%Y-%m-%d %H:%M")
135
+
136
+
137
+ @app.template_filter("shortdatetime")
138
+ def shortdatetime(dt):
139
+ return _fmt(dt, "%b %d %H:%M").lower()
140
+
141
+
142
+ @app.template_filter("proper")
143
+ def proper(s):
144
+ return s.replace("_", " ").title()
145
+
146
+
147
+ def _init_routes(app):
148
+
149
+ @app.route("/manifest.json")
150
+ def manifest():
151
+ return send_from_directory("static", "manifest.json")
152
+
153
+
154
+ def _init_error_handlers(app):
155
+
156
+ @app.errorhandler(CSRFError)
157
+ def csrf_error(e):
158
+ return render_template("base/csrf_error.html"), 400
159
+
160
+
161
+ @app.errorhandler(404)
162
+ def not_found(e):
163
+ return render_template("base/404.html"), 404
164
+
165
+
166
+ def _user_management(app):
167
+ from blueprints.auth.services import load_user, create_user
168
+
169
+ @app.before_request
170
+ def fetch_user():
171
+ user_id = session.get("user_id")
172
+
173
+ if not user_id:
174
+ g.user = None
175
+ return
176
+
177
+ user = load_user(user_id)
178
+
179
+ if not user:
180
+ g.user = None
181
+ return
182
+
183
+ g.user = user
184
+
185
+ @app.cli.command("initdb")
186
+ @with_appcontext
187
+ def initdb():
188
+ import subprocess
189
+
190
+ required = [
191
+ "DEV_MYSQL_USERNAME",
192
+ "DEV_MYSQL_PASS",
193
+ "DEV_MYSQL_HOST",
194
+ "DEV_MYSQL_NAME"
195
+ ]
196
+
197
+ missing = [x for x in required if not os.getenv(x)]
198
+ if missing:
199
+ print(f"Missing env vars: {', '.join(missing)}")
200
+ return
201
+
202
+ subprocess.run(["flask", "db", "init"])
203
+ subprocess.run(["flask", "db", "migrate", "-m", "auto init"])
204
+ subprocess.run(["flask", "db", "upgrade"])
205
+ print("Database initialized.")
206
+
207
+ @app.cli.command("superuser")
208
+ @with_appcontext
209
+ def superuser():
210
+ from getpass import getpass
211
+ import os
212
+
213
+ print("Starting application...")
214
+
215
+ load_models()
216
+
217
+ email = input("Email: ").lower().strip()
218
+ username = input("Username: ").lower().strip()
219
+
220
+ while True:
221
+ password = getpass("Password: ")
222
+ password_confirm = getpass("Again: ")
223
+
224
+ if password == password_confirm:
225
+ break
226
+ print("Passwords do not match")
227
+
228
+ res = create_user(
229
+ username=username,
230
+ email=email,
231
+ role="owner",
232
+ password=password
233
+ )
234
+
235
+ if not res.success:
236
+ print(f"Error: {res.message}")
237
+ return
238
+
239
+ print("\nSuperuser created successfully.")
240
+ print("Run: flask run --debug")
@@ -0,0 +1,71 @@
1
+ from datetime import datetime, timezone
2
+ from db import db
3
+ import uuid
4
+
5
+ def utc_now():
6
+ return datetime.now(timezone.utc)
7
+
8
+ class User(db.Model):
9
+ __tablename__ = "users"
10
+
11
+ user_id = db.Column(
12
+ db.String(36),
13
+ primary_key=True,
14
+ default=lambda: str(uuid.uuid4())
15
+ )
16
+
17
+ username = db.Column(
18
+ db.String(100),
19
+ unique=True,
20
+ nullable=False
21
+ )
22
+
23
+ role = db.Column(
24
+ db.String(50),
25
+ nullable=False,
26
+ default="user"
27
+ )
28
+
29
+ status = db.Column(
30
+ db.String(50),
31
+ nullable=False,
32
+ default="active"
33
+ )
34
+
35
+ password_hash = db.Column(
36
+ db.Text,
37
+ nullable=False
38
+ )
39
+
40
+ email = db.Column(
41
+ db.String(255),
42
+ unique=True,
43
+ nullable=False
44
+ )
45
+
46
+ created_at = db.Column(
47
+ db.DateTime(timezone=True),
48
+ default=utc_now
49
+ )
50
+
51
+ last_seen = db.Column(
52
+ db.DateTime(timezone=True),
53
+ default=utc_now
54
+ )
55
+
56
+ failed_login_attempts = db.Column(
57
+ db.Integer,
58
+ default=0
59
+ )
60
+
61
+ locked_until = db.Column(
62
+ db.DateTime(timezone=True)
63
+ )
64
+
65
+ deactivated_at = db.Column(
66
+ db.DateTime(timezone=True)
67
+ )
68
+
69
+ deleted_at = db.Column(
70
+ db.DateTime(timezone=True)
71
+ )
@@ -0,0 +1,4 @@
1
+ def roles():
2
+ return [
3
+ "user", "admin", "owner", "system"
4
+ ]
@@ -0,0 +1,41 @@
1
+ from flask import render_template, redirect, url_for, request, Blueprint, flash, session
2
+ from tools import genflash
3
+
4
+
5
+ auth_bp = Blueprint(
6
+ "auth_bp",
7
+ __name__,
8
+ template_folder="templates",
9
+ static_folder="static"
10
+ )
11
+
12
+ @auth_bp.route("/login", methods=["GET", "POST"])
13
+ def login():
14
+
15
+ if request.method == 'POST':
16
+ from blueprints.auth.services import authenticate_user
17
+
18
+ res = authenticate_user(
19
+ email=request.form.get("email", "").strip().lower(),
20
+ password=request.form.get("password", "")
21
+ )
22
+
23
+ if not res.success:
24
+ msg, cat = genflash(res.message, "error")
25
+ flash(msg ,cat)
26
+ return redirect(url_for("auth_bp.login"))
27
+
28
+ session["user_id"] = res.data["user_id"]
29
+ msg, cat = genflash("Welcome", "chat")
30
+ flash(msg, cat)
31
+ return redirect(url_for("core_bp.home"))
32
+
33
+ return render_template("auth/login.html")
34
+
35
+
36
+ @auth_bp.post("/logout")
37
+ def logout():
38
+ session.clear()
39
+ msg, cat = genflash("Bye bye!", "chat")
40
+ flash(msg, cat)
41
+ return redirect(url_for("auth_bp.login"))
@@ -0,0 +1,162 @@
1
+ from werkzeug.security import generate_password_hash, check_password_hash
2
+ from blueprints.core.error_logging import log_error
3
+ from datetime import datetime, timezone, timedelta
4
+ from blueprints.auth.roles import roles
5
+ from dataclasses import dataclass
6
+ from typing import Optional, Any
7
+ from models import User
8
+ from flask import g
9
+ from db import db
10
+ import uuid
11
+ import re
12
+
13
+
14
+ @dataclass
15
+ class Result:
16
+ success: bool
17
+ data: Optional[Any] = None
18
+ message: str = ""
19
+
20
+
21
+ def utc_now():
22
+ return datetime.now(timezone.utc)
23
+
24
+
25
+ def valid_email_pattern(email: str) -> bool:
26
+ pattern = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
27
+ return bool(re.match(pattern, email))
28
+
29
+
30
+ def validate_password_strength(password: str) -> Result:
31
+ if len(password) < 8:
32
+ return Result(False, message="Password must be at least 8 characters long.")
33
+
34
+ if not re.search(r"[A-Z]", password):
35
+ return Result(False, message="Password must contain at least one uppercase letter.")
36
+
37
+ if not re.search(r"[a-z]", password):
38
+ return Result(False, message="Password must contain at least one lowercase letter.")
39
+
40
+ if not re.search(r"[^A-Za-z0-9]", password):
41
+ return Result(False, message="Password must contain at least one special character.")
42
+
43
+ return Result(True)
44
+
45
+ def username_exists(username: str) -> bool:
46
+ return User.query.filter_by(username=username).first() is not None
47
+
48
+ def validate_role(role: str) -> bool:
49
+ return role.strip().lower() in {r.lower() for r in roles()}
50
+
51
+
52
+ def email_exists(email: str) -> bool:
53
+ return User.query.filter_by(email=email).first() is not None
54
+
55
+
56
+ def validate_user_creation(username: str, email: str, role: str, password: str) -> Result:
57
+
58
+ if username_exists(username):
59
+ return Result(False, message="Username already taken")
60
+
61
+ if not validate_role(role=role):
62
+ return Result(False, message="Invalid role")
63
+
64
+ if not valid_email_pattern(email):
65
+ return Result(False, message="Invalid email address")
66
+
67
+ if email_exists(email):
68
+ return Result(False, message="Email already registered")
69
+
70
+ password_check = validate_password_strength(password)
71
+ if not password_check.success:
72
+ return password_check
73
+
74
+ return Result(True)
75
+
76
+
77
+ def create_user(username: str, email: str, role: str, password: str) -> Result:
78
+
79
+ validation = validate_user_creation(username, email, role, password)
80
+ if not validation.success:
81
+ return validation
82
+
83
+ user = User(
84
+ user_id=str(uuid.uuid4()),
85
+ username=username,
86
+ email=email,
87
+ role=role,
88
+ password_hash=generate_password_hash(password),
89
+ created_at=utc_now()
90
+ )
91
+ try:
92
+ db.session.add(user)
93
+ db.session.commit()
94
+
95
+ return Result(True, message="User created successfully")
96
+
97
+ except Exception as e:
98
+ log_error(
99
+ user_id=getattr(g, "user_id", None),
100
+ function="create_user",
101
+ action="Creating user",
102
+ exception=e
103
+ )
104
+
105
+ return Result(False, message="An error occurred when creating the user")
106
+
107
+
108
+ def authenticate_user(email, password):
109
+ user = User.query.filter_by(email=email).first()
110
+
111
+ if not user:
112
+ return Result(False, message="invalid credentials")
113
+
114
+ now = datetime.now(timezone.utc)
115
+
116
+ if user.status == "deleted":
117
+ delete_at = user.deactivated_at + timedelta(days=30)
118
+ if delete_at < now:
119
+ message = "This account and all its data was deleted permanently. Please create a new one"
120
+ else:
121
+ message = f"This account will be permanently deleted on {delete_at.strftime('%Y-%m-%d %H:%M UTC')}."
122
+ return Result(False, message=message)
123
+
124
+ now = datetime.now(timezone.utc)
125
+
126
+ if user.locked_until and user.locked_until > now:
127
+ return Result(False, message="Too many attempts. Try again later.")
128
+
129
+ if not check_password_hash(user.password_hash, password):
130
+ user.failed_login_attempts = (user.failed_login_attempts or 0) + 1
131
+
132
+ if user.failed_login_attempts >= 5:
133
+ user.locked_until = now + timedelta(minutes=10)
134
+
135
+ db.session.commit()
136
+
137
+ return Result(False, message="Invalid credentials")
138
+
139
+ user.failed_login_attempts = 0
140
+ user.locked_until = None
141
+ db.session.commit()
142
+
143
+ return Result(
144
+ True,
145
+ data={"user_id": user.user_id},
146
+ message="User authenticated successfully"
147
+ )
148
+
149
+
150
+ def load_user(user_id):
151
+ user = db.session.query(
152
+ User.user_id,
153
+ User.username,
154
+ User.email,
155
+ User.role,
156
+ User.status,
157
+ User.created_at
158
+ ).filter_by(user_id=user_id).first()
159
+ if not user:
160
+ return None
161
+
162
+ return user
@@ -0,0 +1,27 @@
1
+ from db import db
2
+ from blueprints.core.models import Error
3
+ from sqlalchemy.exc import SQLAlchemyError
4
+ from flask import g
5
+ import traceback
6
+
7
+
8
+ def log_error(
9
+ user_id: str,
10
+ function: str,
11
+ action: str,
12
+ exception: Exception
13
+ ):
14
+ try:
15
+ db.session.add(
16
+ Error(
17
+ user_id=user_id,
18
+ function=function,
19
+ action=action,
20
+ error_message=str(exception),
21
+ traceback=traceback.format_exc()
22
+ )
23
+ )
24
+ db.session.commit()
25
+
26
+ except SQLAlchemyError:
27
+ db.session.rollback()
@@ -0,0 +1,21 @@
1
+ from datetime import datetime, timezone
2
+ from db import db
3
+ import uuid
4
+
5
+
6
+ def utc_now():
7
+ return datetime.now(timezone.utc)
8
+
9
+
10
+ class Error(db.Model):
11
+ __tablename__="errors"
12
+
13
+ error_id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
14
+ user_id = db.Column(db.String(36), db.ForeignKey("users.user_id"))
15
+ function = db.Column(db.String(100))
16
+ action = db.Column(db.String(100))
17
+ error = db.Column(db.Text)
18
+ traceback = db.Column(db.Text)
19
+ created_at = db.Column(db.DateTime(timezone=True), default=utc_now, index=True)
20
+
21
+ user = db.relationship("User")
@@ -0,0 +1,19 @@
1
+ from flask import render_template, redirect, url_for, Blueprint, g
2
+ from wrappers import login_required
3
+
4
+ core_bp = Blueprint(
5
+ "core_bp",
6
+ __name__,
7
+ template_folder="templates",
8
+ static_folder="static"
9
+ )
10
+
11
+ @core_bp.route("/")
12
+ @login_required
13
+ def home():
14
+
15
+ context = {
16
+ "selected": "home",
17
+ "user": g.user
18
+ }
19
+ return render_template("/core/home.html", **context)
@@ -0,0 +1,19 @@
1
+ from flask import render_template, redirect, url_for, Blueprint, g
2
+ from wrappers import login_required
3
+
4
+ settings_bp = Blueprint(
5
+ "settings_bp",
6
+ __name__,
7
+ template_folder="templates",
8
+ static_folder="static"
9
+ )
10
+
11
+ @settings_bp.route("/settings")
12
+ @login_required
13
+ def settings():
14
+ context = {
15
+ "selected": "settings",
16
+ "user": g.user,
17
+
18
+ }
19
+ return render_template("/settings/general.html", **context)
@@ -0,0 +1,53 @@
1
+ from datetime import timedelta
2
+ import os
3
+
4
+ class BaseConfig:
5
+ SECRET_KEY = os.getenv("SECRET_KEY")
6
+
7
+ # Fail fast instead of silently breaking later
8
+ if not SECRET_KEY:
9
+ raise ValueError("SECRET_KEY is missing")
10
+
11
+ CRKEY = os.getenv("CRKEY")
12
+
13
+ PERMANENT_SESSION_LIFETIME = timedelta(days=365)
14
+ SESSION_TYPE = "filesystem"
15
+ SESSION_USE_SIGNER = True
16
+ SESSION_FILE_DIR = os.path.join(os.getcwd(), "flask_session")
17
+
18
+ CACHE_TYPE = "SimpleCache"
19
+ CACHE_DEFAULT_TIMEOUT = 300
20
+
21
+ WTF_CSRF_TIME_LIMIT = 3 * 60 * 60
22
+
23
+ SQLALCHEMY_TRACK_MODIFICATIONS = False
24
+
25
+
26
+ class ConfigProd(BaseConfig):
27
+ SESSION_COOKIE_SECURE = True
28
+
29
+ SQLALCHEMY_DATABASE_URI = (
30
+ f"mysql+pymysql://{os.getenv('PROD_MYSQL_USERNAME')}:"
31
+ f"{os.getenv('PROD_MYSQL_PASS')}@"
32
+ f"{os.getenv('PROD_MYSQL_HOST')}/"
33
+ f"{os.getenv('PROD_MYSQL_NAME')}"
34
+ )
35
+
36
+
37
+ class ConfigTest(BaseConfig):
38
+ SESSION_COOKIE_SECURE = False
39
+ CACHE_DEFAULT_TIMEOUT = 60
40
+
41
+ SQLALCHEMY_DATABASE_URI = (
42
+ f"mysql+pymysql://{os.getenv('DEV_MYSQL_USERNAME')}:"
43
+ f"{os.getenv('DEV_MYSQL_PASS')}@"
44
+ f"{os.getenv('DEV_MYSQL_HOST')}/"
45
+ f"{os.getenv('DEV_MYSQL_NAME')}"
46
+ )
47
+
48
+
49
+ config_dict = {
50
+ "prod": ConfigProd,
51
+ "test": ConfigTest,
52
+ "default": ConfigTest
53
+ }
@@ -0,0 +1,5 @@
1
+ from flask_sqlalchemy import SQLAlchemy
2
+ from flask_migrate import Migrate
3
+
4
+ db = SQLAlchemy()
5
+ migrate = Migrate()
@@ -0,0 +1,11 @@
1
+ from blueprints.auth.models import User
2
+ from blueprints.core.models import Error
3
+
4
+ def load_models():
5
+ """
6
+ Import all models in here to be supplied in app.py
7
+ """
8
+ return [User, Error]
9
+
10
+
11
+ __all__ = ["User", "Error"]
@@ -0,0 +1,45 @@
1
+ def genflash(message: str, category: str):
2
+ """
3
+ Accepted categories:
4
+ - bug
5
+ - error
6
+ - warning
7
+ - ok
8
+ - safe
9
+ - win
10
+ - timeout
11
+ - wait
12
+ - announce
13
+ - upload
14
+ - save
15
+ - delete
16
+ - thumbsup
17
+ - thumbsdown
18
+ - chat
19
+ """
20
+
21
+ cats = {
22
+ 'bug': {'icon': 'bug-outline', 'class': 'text-amber-400'},
23
+ 'error': {'icon': 'alert-circle-outline', 'class': 'text-red-400'},
24
+ 'warning': {'icon': 'warning-outline', 'class': 'text-amber-400'},
25
+ 'ok': {'icon': 'checkmark-circle-outline', 'class': 'text-green-400'},
26
+ 'safe': {'icon': 'shield-checkmark-outline', 'class': 'text-teal-400'},
27
+ 'win': {'icon': 'medal-outline', 'class': 'text-sky-400'},
28
+ 'timeout': {'icon': 'alarm-outline', 'class': 'text-amber-400'},
29
+ 'wait': {'icon': 'hourglass-outline', 'class': 'text-amber-400'},
30
+ 'announce': {'icon': 'megaphone-outline', 'class': 'text-teal-400'},
31
+ 'upload': {'icon': 'cloud-upload-outline', 'class': 'text-white'},
32
+ 'save': {'icon': 'save-outline', 'class': 'text-sky-400'},
33
+ 'delete': {'icon': 'trash-outline', 'class': 'text-red-400'},
34
+ 'thumbsup': {'icon': 'thumbs-up-outline', 'class': 'text-green-400'},
35
+ 'thumbsdown': {'icon': 'thumbs-down-outline', 'class': 'text-red-400'},
36
+ 'chat': {'icon': 'chatbubble-ellipses-outline', 'class': 'text-sky-400'},
37
+ }
38
+ if category not in cats:
39
+ raise ValueError("Invalid category")
40
+
41
+ return {
42
+ "text": message,
43
+ "icon": cats[category]['icon'],
44
+ "icon_class": cats[category]['class'],
45
+ }, category
@@ -0,0 +1,13 @@
1
+ from flask import session, redirect, url_for, flash, g
2
+ from functools import wraps
3
+ from tools import genflash
4
+
5
+ def login_required(f):
6
+ @wraps(f)
7
+ def decorated_function(*args, **kwargs):
8
+ if 'user_id' not in session or not g.user:
9
+ msg, cat = genflash("You must log in to visit this page", "error")
10
+ flash(msg, cat)
11
+ return redirect(url_for('auth_bp.login'))
12
+ return f(*args, **kwargs)
13
+ return decorated_function
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.4
2
+ Name: lombik
3
+ Version: 0.1.0
4
+ Summary: Flask scaffold engine
5
+ Author: David Plajner
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: Flask
9
+ Requires-Dist: Flask-SQLAlchemy
10
+ Requires-Dist: Flask-Migrate
11
+ Requires-Dist: Flask-WTF
12
+ Requires-Dist: Flask-Session
13
+ Requires-Dist: python-dotenv
14
+ Requires-Dist: click
15
+ Requires-Dist: SQLAlchemy
16
+ Requires-Dist: Werkzeug
17
+ Dynamic: license-file
18
+
19
+ # Lombik
20
+ #### A scaffold engine for Flask, the greatest framework of all times.
21
+
22
+ This readme will serve perfectly as a self documentation file as I go, and at the end of it I will re-write it with the final details.
23
+ That means that if you see this, either my company cleersoftware has taken off and you might work with me on enhancing the scaffold for our apps.
24
+ Another possibility is that lombik took off, and you are reading the early commits. Have fun, there is probably a lot of them.
25
+ Anyways, I'm gonan actually start on explaning / plannign how thigns will work.
26
+
27
+ > Install lombik
28
+ Run `pip install lombik`
29
+
30
+ > Create app
31
+ To create an app called *myapp*, run `lombik createapp myapp` then `cd myapp`
32
+ This creates the entire structure of your application in one go and you can run `flask run --debug`
33
+
34
+ > Create superuser
35
+ Lombik comes with MySQL as a default db. In the generated .env file you'll find the credentials for local development.
36
+ Replace those placeholders with your actual dev credentials and production if you have it already.
37
+ When done, initialize the database by running `flask initdb` and then you can run `flask superuser` to create your owner account.
38
+ With this, you'll be able to log in to your app and start developing from there.
39
+
40
+ This is a quite simple auth system, in fact it is simple by design and meant to be replaced for production apps with something more robust.
41
+
42
+ > Features and benefits
43
+ On top of the structural guidance, lombik comes with a few built in goodies.
44
+
45
+ - User authentication and session handling
46
+ - CSRF / session expiry
47
+ - Error handling
48
+ - Structure
49
+ - Template filters for frontend development
50
+ - Pre-imported libraries
51
+ - General templates for desktop and mobile
52
+
53
+
@@ -0,0 +1,28 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ lombik/__init__.py
5
+ lombik/cli.py
6
+ lombik.egg-info/PKG-INFO
7
+ lombik.egg-info/SOURCES.txt
8
+ lombik.egg-info/dependency_links.txt
9
+ lombik.egg-info/entry_points.txt
10
+ lombik.egg-info/requires.txt
11
+ lombik.egg-info/top_level.txt
12
+ lombik/templates/__init__.py
13
+ lombik/templates/createapp/app.py
14
+ lombik/templates/createapp/config.py
15
+ lombik/templates/createapp/db.py
16
+ lombik/templates/createapp/tools.py
17
+ lombik/templates/createapp/wrappers.py
18
+ lombik/templates/createapp/blueprints/auth/models.py
19
+ lombik/templates/createapp/blueprints/auth/roles.py
20
+ lombik/templates/createapp/blueprints/auth/routes.py
21
+ lombik/templates/createapp/blueprints/auth/services.py
22
+ lombik/templates/createapp/blueprints/core/error_logging.py
23
+ lombik/templates/createapp/blueprints/core/models.py
24
+ lombik/templates/createapp/blueprints/core/routes.py
25
+ lombik/templates/createapp/blueprints/settings/modesl.py
26
+ lombik/templates/createapp/blueprints/settings/routes.py
27
+ lombik/templates/createapp/blueprints/settings/services.py
28
+ lombik/templates/createapp/models/__init__.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ lombik = lombik.cli:cli
@@ -0,0 +1,9 @@
1
+ Flask
2
+ Flask-SQLAlchemy
3
+ Flask-Migrate
4
+ Flask-WTF
5
+ Flask-Session
6
+ python-dotenv
7
+ click
8
+ SQLAlchemy
9
+ Werkzeug
@@ -0,0 +1 @@
1
+ lombik
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "lombik"
7
+ version = "0.1.0"
8
+ description = "Flask scaffold engine"
9
+ authors = [
10
+ { name="David Plajner" }
11
+ ]
12
+ readme = "README.md"
13
+
14
+ dependencies = [
15
+ "Flask",
16
+ "Flask-SQLAlchemy",
17
+ "Flask-Migrate",
18
+ "Flask-WTF",
19
+ "Flask-Session",
20
+ "python-dotenv",
21
+ "click",
22
+ "SQLAlchemy",
23
+ "Werkzeug"
24
+ ]
25
+
26
+ [project.scripts]
27
+ lombik = "lombik.cli:cli"
lombik-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+