first commit
This commit is contained in:
203
.gitignore
vendored
Normal file
203
.gitignore
vendored
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[codz]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py.cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# UV
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
#uv.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
#poetry.toml
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
||||||
|
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
||||||
|
#pdm.lock
|
||||||
|
#pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# pixi
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
||||||
|
#pixi.lock
|
||||||
|
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
||||||
|
# in the .venv directory. It is recommended not to include this directory in version control.
|
||||||
|
.pixi
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.envrc
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
# Abstra
|
||||||
|
# Abstra is an AI-powered process automation framework.
|
||||||
|
# Ignore directories containing user credentials, local state, and settings.
|
||||||
|
# Learn more at https://abstra.io/docs
|
||||||
|
.abstra/
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||||
|
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||||
|
# you could uncomment the following to ignore the entire vscode folder
|
||||||
|
# .vscode/
|
||||||
|
|
||||||
|
# Ruff stuff:
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# PyPI configuration file
|
||||||
|
.pypirc
|
||||||
|
|
||||||
|
# Marimo
|
||||||
|
marimo/_static/
|
||||||
|
marimo/_lsp/
|
||||||
|
__marimo__/
|
||||||
|
|
||||||
|
# Streamlit
|
||||||
|
.streamlit/secrets.toml
|
||||||
44
app.py
Normal file
44
app.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import os
|
||||||
|
import importlib
|
||||||
|
from flask import Flask, Blueprint
|
||||||
|
from models import db
|
||||||
|
import secrets
|
||||||
|
app = Flask(__name__, static_folder='static', static_url_path='/static')
|
||||||
|
|
||||||
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
|
||||||
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
|
if os.path.isfile("secret.key"):
|
||||||
|
with open("secret.key", "r") as f:
|
||||||
|
app.config['SECRET_KEY'] = f.read().strip()
|
||||||
|
else:
|
||||||
|
app.config['SECRET_KEY'] = secrets.token_hex(16)
|
||||||
|
with open("secret.key", "w") as f:
|
||||||
|
f.write(app.config['SECRET_KEY'])
|
||||||
|
|
||||||
|
def register_blueprints(app, package_name, package_path):
|
||||||
|
"""
|
||||||
|
Auto-register all blueprints found in the package folder.
|
||||||
|
Supports files like index.py as well.
|
||||||
|
Each module must expose a blueprint named `<something>_bp`.
|
||||||
|
"""
|
||||||
|
for filename in os.listdir(package_path):
|
||||||
|
# Accept .py files including index.py, except __init__.py
|
||||||
|
if filename.endswith('.py') and filename != '__init__.py':
|
||||||
|
module_name = filename[:-3] # strip .py
|
||||||
|
module = importlib.import_module(f"{package_name}.{module_name}")
|
||||||
|
|
||||||
|
# Register all blueprint instances in the module
|
||||||
|
for attr_name in dir(module):
|
||||||
|
attr = getattr(module, attr_name)
|
||||||
|
if isinstance(attr, Blueprint):
|
||||||
|
app.register_blueprint(attr)
|
||||||
|
|
||||||
|
register_blueprints(app, 'pages', os.path.join(os.path.dirname(__file__), 'pages'))
|
||||||
|
|
||||||
|
db.init_app(app)
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all() # if you want to create tables
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True)
|
||||||
3
goodrun.sh
Normal file
3
goodrun.sh
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
source ./.venv/bin/activate
|
||||||
|
gunicorn -w 4 -b 0.0.0.0:8000 app:app
|
||||||
17
models.py
Normal file
17
models.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
|
db = SQLAlchemy()
|
||||||
|
|
||||||
|
class User(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||||
|
password = db.Column(db.String(128), nullable=False) # hashed password storage
|
||||||
|
register_time = db.Column(db.DateTime, nullable=False)
|
||||||
|
|
||||||
|
class Finding(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
title = db.Column(db.String(120), nullable=False)
|
||||||
|
path = db.Column(db.String(120), nullable=False) # Path on laminax.org
|
||||||
|
find_time = db.Column(db.DateTime, nullable=False)
|
||||||
|
found_by = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
content_preview = db.Column(db.Text, nullable=True) # Scraped preview of the finding
|
||||||
122
pages/findings.py
Normal file
122
pages/findings.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
from flask import Blueprint, render_template
|
||||||
|
from models import Finding, User
|
||||||
|
from sqlalchemy import desc
|
||||||
|
|
||||||
|
findings_bp = Blueprint('findings', __name__, url_prefix='/findings')
|
||||||
|
|
||||||
|
@findings_bp.route('/')
|
||||||
|
def latest_findings():
|
||||||
|
latest = Finding.query.order_by(desc(Finding.find_time)).limit(20).all()
|
||||||
|
# Eager load user data if needed
|
||||||
|
user_map = {u.id: u for u in User.query.filter(User.id.in_([f.found_by for f in latest])).all()}
|
||||||
|
return render_template('latest_findings.html', findings=latest, user_map=user_map)
|
||||||
|
|
||||||
|
@findings_bp.route('/<int:finding_id>')
|
||||||
|
def finding_detail(finding_id):
|
||||||
|
finding = Finding.query.get_or_404(finding_id)
|
||||||
|
user = User.query.get(finding.found_by)
|
||||||
|
return render_template('finding_detail.html', finding=finding, user=user)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from flask import Blueprint, render_template, request, session, flash, redirect, url_for
|
||||||
|
from datetime import datetime
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from models import db, Finding
|
||||||
|
|
||||||
|
|
||||||
|
@findings_bp.route('/create', methods=['GET', 'POST'])
|
||||||
|
def create_finding():
|
||||||
|
if not session.get('loggedin'):
|
||||||
|
flash("Please log in to create a finding.", "warning")
|
||||||
|
return redirect(url_for('login.login'))
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
path = request.form.get('path', '').strip()
|
||||||
|
lorekey = request.form.get('lorekey', '').strip()
|
||||||
|
|
||||||
|
# Validate inputs
|
||||||
|
if not path and not lorekey:
|
||||||
|
flash("Title, Path, and Lorekey are required.", "danger")
|
||||||
|
return render_template('create_finding.html', path=path, lorekey=lorekey)
|
||||||
|
|
||||||
|
# Validate path exists on laminax.org (non-404)
|
||||||
|
if path:
|
||||||
|
try:
|
||||||
|
path_res = requests.get(f'https://laminax.org/{path}')
|
||||||
|
if path_res.status_code == 404:
|
||||||
|
flash(f"The path '{path}' does not exist on laminax.org.", "danger")
|
||||||
|
return render_template('create_finding.html', path=path, lorekey=lorekey)
|
||||||
|
else:
|
||||||
|
soup = BeautifulSoup(path_res.text, 'html.parser')
|
||||||
|
for hr in soup.find_all('hr'):
|
||||||
|
hr.replace_with('----------')
|
||||||
|
content_text = soup.get_text(separator='\n')
|
||||||
|
content_text = soup.get_text(separator='\n')
|
||||||
|
# Get title element
|
||||||
|
title = (soup.title.string if soup.title else None) or "No title found"
|
||||||
|
# Save finding
|
||||||
|
new_finding = Finding(
|
||||||
|
title=f'https://laminax.org/{path}',
|
||||||
|
path=f'https://laminax.org/{path}',
|
||||||
|
find_time=datetime.utcnow(),
|
||||||
|
found_by=session.get('id'),
|
||||||
|
content_preview=content_text
|
||||||
|
)
|
||||||
|
db.session.add(new_finding)
|
||||||
|
db.session.commit()
|
||||||
|
flash("Finding created successfully!", "success")
|
||||||
|
return redirect("/findings/"+str(new_finding.id)) # Resort to manually redirecting for now
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Error validating path: {e}", "danger")
|
||||||
|
return render_template('create_finding.html', path=path, lorekey=lorekey)
|
||||||
|
|
||||||
|
# Check lorekey with external service
|
||||||
|
if lorekey:
|
||||||
|
try:
|
||||||
|
res = requests.post('https://worker.laminax.org/check-password', json={"password": lorekey})
|
||||||
|
if res.ok:
|
||||||
|
data = res.json()
|
||||||
|
if data.get('redirect'):
|
||||||
|
redirect_url = data['redirect']
|
||||||
|
|
||||||
|
# Fetch redirect page content
|
||||||
|
page_res = requests.get(redirect_url)
|
||||||
|
title = None
|
||||||
|
if page_res.ok:
|
||||||
|
# Parse html and replace all <hr> with 10 dashes using bs4
|
||||||
|
soup = BeautifulSoup(page_res.text, 'html.parser')
|
||||||
|
for hr in soup.find_all('hr'):
|
||||||
|
hr.replace_with('----------')
|
||||||
|
content_text = soup.get_text(separator='\n')
|
||||||
|
# Get title element
|
||||||
|
title = (soup.title.string if soup.title else None) or "No title found"
|
||||||
|
else:
|
||||||
|
content_text = None
|
||||||
|
title = "Unable to fetch redirect page content."
|
||||||
|
|
||||||
|
# Save finding
|
||||||
|
new_finding = Finding(
|
||||||
|
title=redirect_url,
|
||||||
|
path=redirect_url,
|
||||||
|
find_time=datetime.utcnow(),
|
||||||
|
found_by=session.get('id'),
|
||||||
|
content_preview=content_text
|
||||||
|
)
|
||||||
|
db.session.add(new_finding)
|
||||||
|
db.session.commit()
|
||||||
|
flash("Finding created successfully!", "success")
|
||||||
|
return redirect(url_for('findings.finding_detail', finding_id=new_finding.id))
|
||||||
|
else:
|
||||||
|
flash("Lorekey check failed or no redirect returned.", "danger")
|
||||||
|
elif res.status_code == 401:
|
||||||
|
flash("Invalid Lorekey provided.", "danger")
|
||||||
|
else:
|
||||||
|
flash("Lorekey service error, try again later.", "danger")
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"An error occurred: {e}", "danger")
|
||||||
|
|
||||||
|
# GET or fallback render
|
||||||
|
return render_template('create_finding.html')
|
||||||
|
|
||||||
7
pages/index.py
Normal file
7
pages/index.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from flask import Blueprint, render_template
|
||||||
|
from models import db, User
|
||||||
|
index_bp = Blueprint('index', __name__)
|
||||||
|
|
||||||
|
@index_bp.route('/')
|
||||||
|
def index():
|
||||||
|
return render_template('home.html',users=User.query.limit(20).all()) # or your index page template
|
||||||
56
pages/login.py
Normal file
56
pages/login.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from flask import Blueprint, render_template, request, redirect, url_for, session
|
||||||
|
from werkzeug.security import check_password_hash
|
||||||
|
from models import db, User
|
||||||
|
|
||||||
|
login_bp = Blueprint('login', __name__, url_prefix='/login')
|
||||||
|
|
||||||
|
@login_bp.route('/', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
if session.get('loggedin'):
|
||||||
|
return redirect(url_for('index.index'))
|
||||||
|
|
||||||
|
username = ""
|
||||||
|
username_err = ""
|
||||||
|
password_err = ""
|
||||||
|
login_err = ""
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
username = request.form.get('username', '').strip()
|
||||||
|
password = request.form.get('password', '').strip()
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
username_err = "Please enter username."
|
||||||
|
if not password:
|
||||||
|
password_err = "Please enter your password."
|
||||||
|
|
||||||
|
if not username_err and not password_err:
|
||||||
|
# Admin bypass (same as before) but don't do this in production!
|
||||||
|
if False: # username == "adm" and password == "dont add this in please":
|
||||||
|
session['loggedin'] = True
|
||||||
|
session['id'] = -1
|
||||||
|
session['username'] = "Admin"
|
||||||
|
return redirect(url_for('index.index'))
|
||||||
|
|
||||||
|
# Query User via SQLAlchemy
|
||||||
|
user = User.query.filter_by(username=username).first()
|
||||||
|
|
||||||
|
if user:
|
||||||
|
# Here you need to store hashed passwords somewhere
|
||||||
|
# Your User model doesn't have a password field yet, so let's assume:
|
||||||
|
# You should add it like: password = db.Column(db.String(128), nullable=False)
|
||||||
|
# For now, assuming you have a password attribute
|
||||||
|
if hasattr(user, 'password') and check_password_hash(user.password, password):
|
||||||
|
session['loggedin'] = True
|
||||||
|
session['id'] = user.id
|
||||||
|
session['username'] = user.username
|
||||||
|
return redirect(url_for('index.index'))
|
||||||
|
else:
|
||||||
|
login_err = "Invalid username or password."
|
||||||
|
else:
|
||||||
|
login_err = "Invalid username or password."
|
||||||
|
|
||||||
|
return render_template('login.html',
|
||||||
|
username=username,
|
||||||
|
username_err=username_err,
|
||||||
|
password_err=password_err,
|
||||||
|
login_err=login_err)
|
||||||
8
pages/logout.py
Normal file
8
pages/logout.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from flask import Blueprint, session, redirect, url_for
|
||||||
|
|
||||||
|
logout_bp = Blueprint('logout', __name__)
|
||||||
|
|
||||||
|
@logout_bp.route('/logout')
|
||||||
|
def logout():
|
||||||
|
session.clear() # Clear all session data
|
||||||
|
return redirect(url_for('login.login')) # Redirect to login page
|
||||||
28
pages/profile.py
Normal file
28
pages/profile.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from flask import Blueprint, render_template, session, redirect, url_for
|
||||||
|
from models import User, Finding
|
||||||
|
|
||||||
|
profile_bp = Blueprint('profile', __name__, url_prefix='/profile')
|
||||||
|
|
||||||
|
@profile_bp.route('/')
|
||||||
|
def my_findings():
|
||||||
|
# Check if user is logged in
|
||||||
|
if not session.get('loggedin'):
|
||||||
|
return redirect(url_for('login.login'))
|
||||||
|
|
||||||
|
user_id = session.get('id')
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
if not user:
|
||||||
|
return redirect(url_for('login.login'))
|
||||||
|
|
||||||
|
# Get all findings by this user, exclude content_preview
|
||||||
|
findings = Finding.query.filter_by(found_by=user_id).all()
|
||||||
|
|
||||||
|
return render_template('profile.html', user=user, findings=findings)
|
||||||
|
|
||||||
|
@profile_bp.route('/get/<int:id>', methods=['GET'])
|
||||||
|
def view_profile(id):
|
||||||
|
user = User.query.get(id)
|
||||||
|
if not user:
|
||||||
|
return "User not found. Please try again later.",
|
||||||
|
findings = Finding.query.filter_by(found_by=id).all()
|
||||||
|
return render_template('view_profile.html', user=user, findings=findings)
|
||||||
68
pages/register.py
Normal file
68
pages/register.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
||||||
|
from werkzeug.security import generate_password_hash
|
||||||
|
from models import db, User
|
||||||
|
|
||||||
|
register_bp = Blueprint('register', __name__, url_prefix='/register')
|
||||||
|
|
||||||
|
@register_bp.route('/', methods=['GET', 'POST'])
|
||||||
|
def register():
|
||||||
|
username = ''
|
||||||
|
password = ''
|
||||||
|
confirm_password = ''
|
||||||
|
username_err = ''
|
||||||
|
password_err = ''
|
||||||
|
confirm_password_err = ''
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
username = request.form.get('username', '').strip()
|
||||||
|
password = request.form.get('password', '').strip()
|
||||||
|
confirm_password = request.form.get('confirm_password', '').strip()
|
||||||
|
|
||||||
|
# Validate username
|
||||||
|
if not username:
|
||||||
|
username_err = "Please enter a username."
|
||||||
|
elif not re.match(r'^[a-zA-Z0-9_]+$', username):
|
||||||
|
username_err = "Username can only contain letters, numbers, and underscores."
|
||||||
|
else:
|
||||||
|
# Check if username already exists
|
||||||
|
if User.query.filter_by(username=username).first():
|
||||||
|
username_err = "This username is already taken."
|
||||||
|
|
||||||
|
# Validate password
|
||||||
|
if not password:
|
||||||
|
password_err = "Please enter a password."
|
||||||
|
elif len(password) < 6:
|
||||||
|
password_err = "Password must have at least 6 characters."
|
||||||
|
|
||||||
|
# Validate confirm password
|
||||||
|
if not confirm_password:
|
||||||
|
confirm_password_err = "Please confirm password."
|
||||||
|
elif password != confirm_password:
|
||||||
|
confirm_password_err = "Password did not match."
|
||||||
|
|
||||||
|
# If no errors, insert new user
|
||||||
|
if not username_err and not password_err and not confirm_password_err:
|
||||||
|
hashed_password = generate_password_hash(password)
|
||||||
|
new_user = User(
|
||||||
|
username=username,
|
||||||
|
password=hashed_password,
|
||||||
|
register_time=datetime.utcnow()
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
db.session.add(new_user)
|
||||||
|
db.session.commit()
|
||||||
|
flash("Registration successful! Please login.", "success")
|
||||||
|
return redirect(url_for('login.login'))
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
flash("Oops! Something went wrong. Please try again.", "danger")
|
||||||
|
|
||||||
|
return render_template('register.html',
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
confirm_password=confirm_password,
|
||||||
|
username_err=username_err,
|
||||||
|
password_err=password_err,
|
||||||
|
confirm_password_err=confirm_password_err)
|
||||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
flask
|
||||||
|
flask-cors
|
||||||
|
flask_sqlalchemy
|
||||||
|
requests
|
||||||
|
beautifulsoup4
|
||||||
|
lxml
|
||||||
|
python-dotenv
|
||||||
1
secret.key
Normal file
1
secret.key
Normal file
@@ -0,0 +1 @@
|
|||||||
|
aea68160bc8c772c266e5dbdb40b2023
|
||||||
137
static/main.css
Normal file
137
static/main.css
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation styles you already have */
|
||||||
|
.topnav {
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topnav a {
|
||||||
|
float: left;
|
||||||
|
color: #f2f2f2;
|
||||||
|
text-align: center;
|
||||||
|
padding: 14px 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topnav a:hover {
|
||||||
|
background-color: #ddd;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topnav a.active {
|
||||||
|
background-color: #04AA6D;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form container wrapper */
|
||||||
|
.wrapper {
|
||||||
|
max-width: 480px;
|
||||||
|
margin: 30px auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 0 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form group spacing */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form labels */
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form inputs */
|
||||||
|
input[type="text"],
|
||||||
|
input[type="password"],
|
||||||
|
input[type="email"],
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.2s ease-in-out;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus,
|
||||||
|
input[type="password"]:focus,
|
||||||
|
input[type="email"]:focus,
|
||||||
|
textarea:focus,
|
||||||
|
select:focus {
|
||||||
|
border-color: #04AA6D;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 4px #04AA6D;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
button.btn, input[type="submit"], input[type="reset"] {
|
||||||
|
padding: 10px 18px;
|
||||||
|
font-size: 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
background-color: #04AA6D;
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.btn:hover,
|
||||||
|
input[type="submit"]:hover,
|
||||||
|
input[type="reset"]:hover {
|
||||||
|
background-color: #039a5b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Secondary button */
|
||||||
|
input[type="reset"] {
|
||||||
|
background-color: #6c757d;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="reset"]:hover {
|
||||||
|
background-color: #565e64;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form feedback text */
|
||||||
|
.form-text {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flash message styling */
|
||||||
|
.flash-message {
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message.success {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message.danger {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message.warning {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
border: 1px solid #ffeeba;
|
||||||
|
}
|
||||||
31
templates/base.html
Normal file
31
templates/base.html
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<!-- base.html -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>{% block title %}My Site{% endblock %}</title>
|
||||||
|
<link rel="stylesheet" href="/static/main.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{% include 'topbar.html' %}
|
||||||
|
<!-- Flashed messages -->
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="flashed-messages">
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
<!-- This block is the “Outlet” equivalent -->
|
||||||
|
<main>
|
||||||
|
{% block content %}
|
||||||
|
<!-- Child templates will fill this -->
|
||||||
|
{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
22
templates/create_finding.html
Normal file
22
templates/create_finding.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Create Finding{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Create a New Finding</h2>
|
||||||
|
|
||||||
|
<form method="post" action="{{ url_for('findings.create_finding') }}">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="path">Path (on laminax.org, optional if lorekey is provided)</label>
|
||||||
|
<input id="path" name="path" class="form-control" type="text" value="{{ path or '' }}">
|
||||||
|
<small class="form-text text-muted">Example: some/path/here</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="lorekey">Lorekey</label>
|
||||||
|
<input id="lorekey" name="lorekey" class="form-control" type="text" value="{{ lorekey or '' }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">Create Finding</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
14
templates/finding_detail.html
Normal file
14
templates/finding_detail.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Finding: {{ finding.title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>{{ finding.title }}</h2>
|
||||||
|
<p><strong>Path:</strong> <a href="{{ finding.path }}">{{ finding.path }}</a></p>
|
||||||
|
<p><strong>Found by:</strong> {{ user.username }}</p>
|
||||||
|
<p><strong>Found on:</strong> {{ finding.find_time.strftime('%Y-%m-%d %H:%M') }}</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<h3>Content Preview</h3>
|
||||||
|
<pre>{{ finding.content_preview or 'No preview available.' }}</pre>
|
||||||
|
{% endblock %}
|
||||||
0
templates/findings.html
Normal file
0
templates/findings.html
Normal file
18
templates/home.html
Normal file
18
templates/home.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<!-- home.html -->
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Home - LAMINAX.ORG ARG findings{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Welcome to ARG findings!</h1>
|
||||||
|
<p>This site is dedicated to sharing the ARG findings at <a href="https://laminax.org">laminax.org</a></p>
|
||||||
|
<!--Render latest users-->
|
||||||
|
<div class="latest-users">
|
||||||
|
<h2>Latest Users</h2>
|
||||||
|
<ul>
|
||||||
|
{% for user in users %}
|
||||||
|
<li>{{ user.username }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
21
templates/latest_findings.html
Normal file
21
templates/latest_findings.html
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Latest Findings{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Latest Findings</h2>
|
||||||
|
|
||||||
|
{% if findings %}
|
||||||
|
<ul>
|
||||||
|
{% for f in findings %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('findings.finding_detail', finding_id=f.id) }}">{{ f.title }}</a>
|
||||||
|
by {{ user_map[f.found_by].username if f.found_by in user_map else 'Unknown' }}
|
||||||
|
— {{ f.find_time.strftime('%Y-%m-%d %H:%M') }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p>No findings yet.</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
41
templates/login.html
Normal file
41
templates/login.html
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Login{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="wrapper" style="max-width:360px; padding:20px; margin:auto;">
|
||||||
|
<h2>Login</h2>
|
||||||
|
<p>Please fill in your credentials to login.</p>
|
||||||
|
|
||||||
|
{% if login_err %}
|
||||||
|
<div class="alert alert-danger">{{ login_err }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" action="{{ url_for('login.login') }}">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Username</label>
|
||||||
|
<input type="text" name="username"
|
||||||
|
class="form-control {% if username_err %}is-invalid{% endif %}"
|
||||||
|
value="{{ username }}">
|
||||||
|
{% if username_err %}
|
||||||
|
<div class="invalid-feedback">{{ username_err }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Password</label>
|
||||||
|
<input type="password" name="password"
|
||||||
|
class="form-control {% if password_err %}is-invalid{% endif %}">
|
||||||
|
{% if password_err %}
|
||||||
|
<div class="invalid-feedback">{{ password_err }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="submit" class="btn btn-primary" value="Login">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Don't have an account? <a href="{{ url_for('register.register') }}">Sign up now</a>.</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
23
templates/profile.html
Normal file
23
templates/profile.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}My Findings{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>My Findings</h2>
|
||||||
|
<p>Welcome, {{ user.username }}!</p>
|
||||||
|
|
||||||
|
{% if findings %}
|
||||||
|
<ul>
|
||||||
|
{% for finding in findings %}
|
||||||
|
<li>
|
||||||
|
<strong>{{ finding.title }}</strong> —
|
||||||
|
Path: <a href="/findings/{{ finding.id }}">{{ finding.path }}</a> —
|
||||||
|
Found on: {{ finding.find_time.strftime('%Y-%m-%d %H:%M') }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p>You have no findings yet.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
49
templates/register.html
Normal file
49
templates/register.html
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Sign Up{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="wrapper" style="max-width:360px; padding:20px; margin:auto;">
|
||||||
|
<h2>Sign Up</h2>
|
||||||
|
<p>Please fill this form to create an account.</p>
|
||||||
|
|
||||||
|
<form method="post" action="{{ url_for('register.register') }}">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Username</label>
|
||||||
|
<input type="text" name="username"
|
||||||
|
class="form-control {% if username_err %}is-invalid{% endif %}"
|
||||||
|
value="{{ username }}">
|
||||||
|
{% if username_err %}
|
||||||
|
<div class="invalid-feedback">{{ username_err }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Password</label>
|
||||||
|
<input type="password" name="password"
|
||||||
|
class="form-control {% if password_err %}is-invalid{% endif %}"
|
||||||
|
value="{{ password }}">
|
||||||
|
{% if password_err %}
|
||||||
|
<div class="invalid-feedback">{{ password_err }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Confirm Password</label>
|
||||||
|
<input type="password" name="confirm_password"
|
||||||
|
class="form-control {% if confirm_password_err %}is-invalid{% endif %}"
|
||||||
|
value="{{ confirm_password }}">
|
||||||
|
{% if confirm_password_err %}
|
||||||
|
<div class="invalid-feedback">{{ confirm_password_err }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="submit" class="btn btn-primary" value="Submit">
|
||||||
|
<input type="reset" class="btn btn-secondary ml-2" value="Reset">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Already have an account? <a href="{{ url_for('login.login') }}">Login here</a>.</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
15
templates/topbar.html
Normal file
15
templates/topbar.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<div class="topnav">
|
||||||
|
<a class="active" href="/">Home</a>
|
||||||
|
<a href="{{ url_for('findings.latest_findings') }}">Latest findings</a>
|
||||||
|
<a href="https://laminax.co">Main site</a>
|
||||||
|
<!-- Additional links for login/register if not logged in, else show user profile and logout -->
|
||||||
|
{% if session.get('loggedin') %}
|
||||||
|
<a href="{{ url_for('logout.logout') }}">Log out</a>
|
||||||
|
<a href="{{ url_for('profile.my_findings') }}">My Findings</a>
|
||||||
|
<a href="{{ url_for('findings.create_finding') }}">Add Finding</a>
|
||||||
|
<a href="/profile/get/{{ session.get('id') }}">My Profile</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ url_for('login.login') }}">Login</a>
|
||||||
|
<a href="{{ url_for('register.register') }}">Register</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
22
templates/view_profile.html
Normal file
22
templates/view_profile.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}My Findings{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ user.username }}'s profile:</h1>
|
||||||
|
<p>Their Findings</p>
|
||||||
|
{% if findings %}
|
||||||
|
<ul>
|
||||||
|
{% for finding in findings %}
|
||||||
|
<li>
|
||||||
|
<strong>{{ finding.title }}</strong> —
|
||||||
|
Path: <a href="/findings/{{ finding.id }}">{{ finding.path }}</a> —
|
||||||
|
Found on: {{ finding.find_time.strftime('%Y-%m-%d %H:%M') }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p>You have no findings yet.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user