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