06/15 Fix issues
This commit is contained in:
@@ -80,13 +80,13 @@ def login():
|
|||||||
user.last_login_at = datetime.utcnow()
|
user.last_login_at = datetime.utcnow()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
nxt = request.args.get("next")
|
nxt = request.args.get("next")
|
||||||
if nxt and nxt.startswith("/"):
|
if nxt and nxt.startswith("/") and not nxt.startswith("//"):
|
||||||
return redirect(nxt)
|
return redirect(nxt)
|
||||||
return redirect(url_for("main.index"))
|
return redirect(url_for("main.index"))
|
||||||
return render_template("auth/login.html", form=form)
|
return render_template("auth/login.html", form=form)
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route("/logout")
|
@auth_bp.route("/logout", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def logout():
|
def logout():
|
||||||
logout_user()
|
logout_user()
|
||||||
@@ -119,7 +119,7 @@ def reset_request():
|
|||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
user = User.query.filter_by(email=form.email.data.lower()).first()
|
user = User.query.filter_by(email=form.email.data.lower()).first()
|
||||||
if user:
|
if user:
|
||||||
token = generate_token(user.id, _RESET_SALT)
|
token = generate_token((user.id, user.password_hash[:20]), _RESET_SALT)
|
||||||
link = url_for("auth.reset_password", token=token, _external=True)
|
link = url_for("auth.reset_password", token=token, _external=True)
|
||||||
send_email(user.email, _("Reset your password"),
|
send_email(user.email, _("Reset your password"),
|
||||||
_("Reset link: %(link)s", link=link))
|
_("Reset link: %(link)s", link=link))
|
||||||
@@ -131,14 +131,19 @@ def reset_request():
|
|||||||
|
|
||||||
@auth_bp.route("/reset/<token>", methods=["GET", "POST"])
|
@auth_bp.route("/reset/<token>", methods=["GET", "POST"])
|
||||||
def reset_password(token):
|
def reset_password(token):
|
||||||
user_id = read_token(token, _RESET_SALT,
|
payload = read_token(token, _RESET_SALT,
|
||||||
current_app.config["TOKEN_RESET_MAX_AGE"])
|
current_app.config["TOKEN_RESET_MAX_AGE"])
|
||||||
if user_id is None:
|
# payload is (user_id, pw_fingerprint) — fingerprint invalidates on use
|
||||||
|
if not isinstance(payload, (list, tuple)) or len(payload) != 2:
|
||||||
flash(_("Reset link is invalid or expired."), "danger")
|
flash(_("Reset link is invalid or expired."), "danger")
|
||||||
return redirect(url_for("auth.reset_request"))
|
return redirect(url_for("auth.reset_request"))
|
||||||
|
user_id, pw_fingerprint = payload
|
||||||
user = db.session.get(User, user_id)
|
user = db.session.get(User, user_id)
|
||||||
if user is None:
|
if user is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
if user.password_hash[:20] != pw_fingerprint:
|
||||||
|
flash(_("Reset link has already been used."), "danger")
|
||||||
|
return redirect(url_for("auth.reset_request"))
|
||||||
form = ResetForm()
|
form = ResetForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
user.set_password(form.password.data)
|
user.set_password(form.password.data)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Order: explicit session choice -> authenticated user.locale -> Accept-Language -
|
|||||||
"""
|
"""
|
||||||
from flask import Blueprint, session, redirect, request, current_app, url_for
|
from flask import Blueprint, session, redirect, request, current_app, url_for
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
from app.utils import safe_referrer
|
||||||
|
|
||||||
i18n_bp = Blueprint("i18n", __name__)
|
i18n_bp = Blueprint("i18n", __name__)
|
||||||
|
|
||||||
@@ -34,5 +35,4 @@ def set_lang(code):
|
|||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
current_user.locale = code
|
current_user.locale = code
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
target = request.referrer or url_for("main.index")
|
return redirect(safe_referrer(url_for("main.index")))
|
||||||
return redirect(target)
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from app.services import messaging as msvc
|
|||||||
from app.services import favorites as fsvc
|
from app.services import favorites as fsvc
|
||||||
from app.services.contact import contact_revealed, mask_body
|
from app.services.contact import contact_revealed, mask_body
|
||||||
from app.blueprints.messaging.forms import MessageForm
|
from app.blueprints.messaging.forms import MessageForm
|
||||||
|
from app.utils import safe_referrer
|
||||||
|
|
||||||
messaging_bp = Blueprint("messaging", __name__)
|
messaging_bp = Blueprint("messaging", __name__)
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ def conversation(conv_id):
|
|||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
msvc.mark_conversation_read(conv, current_user)
|
msvc.mark_conversation_read(conv, current_user)
|
||||||
|
# reveal controls the "contact info hidden" banner for the current reader
|
||||||
reveal = contact_revealed(current_user)
|
reveal = contact_revealed(current_user)
|
||||||
form = MessageForm()
|
form = MessageForm()
|
||||||
|
|
||||||
@@ -47,9 +49,10 @@ def conversation(conv_id):
|
|||||||
except msvc.MessagingError as e:
|
except msvc.MessagingError as e:
|
||||||
flash(str(e), "danger")
|
flash(str(e), "danger")
|
||||||
|
|
||||||
# mask contact info in messages for low-trust users
|
# Mask is based on the *sender's* trust so a low-trust sender cannot
|
||||||
|
# slip contact info through to a trusted reader.
|
||||||
masked_messages = [
|
masked_messages = [
|
||||||
(msg, mask_body(msg.body, reveal=reveal))
|
(msg, mask_body(msg.body, reveal=contact_revealed(msg.sender)))
|
||||||
for msg in conv.messages
|
for msg in conv.messages
|
||||||
]
|
]
|
||||||
return render_template("messaging/conversation.html",
|
return render_template("messaging/conversation.html",
|
||||||
@@ -103,8 +106,8 @@ def toggle_favorite(listing_id):
|
|||||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||||
return jsonify(favorited=now_fav)
|
return jsonify(favorited=now_fav)
|
||||||
flash(_("Saved.") if now_fav else _("Removed from saved."), "info")
|
flash(_("Saved.") if now_fav else _("Removed from saved."), "info")
|
||||||
return redirect(request.referrer or url_for("listings.detail",
|
return redirect(safe_referrer(url_for("listings.detail",
|
||||||
listing_id=listing_id))
|
listing_id=listing_id)))
|
||||||
|
|
||||||
|
|
||||||
@messaging_bp.route("/my/favorites")
|
@messaging_bp.route("/my/favorites")
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ from app.models.enums import TrustTier
|
|||||||
# patterns
|
# patterns
|
||||||
_PHONE_RE = re.compile(
|
_PHONE_RE = re.compile(
|
||||||
r"(\+?1[\s\-.]?)?"
|
r"(\+?1[\s\-.]?)?"
|
||||||
r"(\(?\d{3}\)?[\s\-.])"
|
r"(\(?\d{3}\)?[\s\-.]?)"
|
||||||
r"\d{3}[\s\-.]\d{4}"
|
r"\d{3}[\s\-.]?\d{4}"
|
||||||
)
|
)
|
||||||
_EMAIL_RE = re.compile(r"[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}")
|
_EMAIL_RE = re.compile(r"[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}")
|
||||||
_URL_RE = re.compile(r"https?://\S+|www\.\S+", re.I)
|
_URL_RE = re.compile(r"https?://\S+|www\.\S+", re.I)
|
||||||
|
|||||||
+2
-1
@@ -4,6 +4,7 @@ Portable across MySQL and SQLite (no spatial extension needed). For large-scale
|
|||||||
deployments, see README for the MySQL POINT + SPATIAL INDEX upgrade.
|
deployments, see README for the MySQL POINT + SPATIAL INDEX upgrade.
|
||||||
"""
|
"""
|
||||||
import math
|
import math
|
||||||
|
from app.extensions import db
|
||||||
from app.models.geo import ZipGeo
|
from app.models.geo import ZipGeo
|
||||||
|
|
||||||
EARTH_MI = 3958.7613 # mean earth radius, miles
|
EARTH_MI = 3958.7613 # mean earth radius, miles
|
||||||
@@ -11,7 +12,7 @@ EARTH_MI = 3958.7613 # mean earth radius, miles
|
|||||||
|
|
||||||
def geocode_zip(zip_code):
|
def geocode_zip(zip_code):
|
||||||
"""Return (lat, lng, city, state, metro) or None."""
|
"""Return (lat, lng, city, state, metro) or None."""
|
||||||
row = ZipGeo.query.get((zip_code or "").strip())
|
row = db.session.get(ZipGeo, (zip_code or "").strip())
|
||||||
if row is None:
|
if row is None:
|
||||||
return None
|
return None
|
||||||
return row.lat, row.lng, row.city, row.state, row.metro
|
return row.lat, row.lng, row.city, row.state, row.metro
|
||||||
|
|||||||
@@ -130,3 +130,7 @@ table.list td{padding:8px 10px;border-bottom:1px solid var(--line)}
|
|||||||
.reply-box{padding:16px}
|
.reply-box{padding:16px}
|
||||||
.mask-notice{background:#fdf4e3;border:1px solid #f0dcae;border-radius:8px;
|
.mask-notice{background:#fdf4e3;border:1px solid #f0dcae;border-radius:8px;
|
||||||
padding:8px 12px;margin-top:8px}
|
padding:8px 12px;margin-top:8px}
|
||||||
|
|
||||||
|
.link-btn{background:none;border:none;padding:0;color:inherit;font:inherit;cursor:pointer;text-decoration:none}
|
||||||
|
.link-btn:hover{text-decoration:underline}
|
||||||
|
.logout-form{display:inline}
|
||||||
|
|||||||
@@ -22,7 +22,10 @@
|
|||||||
{% if unread_count %}<span class="badge-count">{{ unread_count }}</span>{% endif %}
|
{% if unread_count %}<span class="badge-count">{{ unread_count }}</span>{% endif %}
|
||||||
</a>
|
</a>
|
||||||
<span class="hi">{{ current_user.display_name }}</span>
|
<span class="hi">{{ current_user.display_name }}</span>
|
||||||
<a href="{{ url_for('auth.logout') }}">{{ _('Sign out') }}</a>
|
<form method="post" action="{{ url_for('auth.logout') }}" class="logout-form">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<button type="submit" class="link-btn">{{ _('Sign out') }}</button>
|
||||||
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('auth.login') }}">{{ _('Sign in') }}</a>
|
<a href="{{ url_for('auth.login') }}">{{ _('Sign in') }}</a>
|
||||||
<a class="btn" href="{{ url_for('auth.register') }}">{{ _('Register') }}</a>
|
<a class="btn" href="{{ url_for('auth.register') }}">{{ _('Register') }}</a>
|
||||||
|
|||||||
+13
-2
@@ -1,10 +1,21 @@
|
|||||||
"""RBAC decorators. Never trust the client; gate on server side."""
|
"""RBAC decorators and shared request utilities."""
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from flask import abort
|
from urllib.parse import urlparse
|
||||||
|
from flask import abort, request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from app.models.enums import Role
|
from app.models.enums import Role
|
||||||
|
|
||||||
|
|
||||||
|
def safe_referrer(fallback: str) -> str:
|
||||||
|
"""Return request.referrer only when it is same-origin; else fallback."""
|
||||||
|
ref = request.referrer
|
||||||
|
if ref:
|
||||||
|
parsed = urlparse(ref)
|
||||||
|
if parsed.netloc in ("", request.host):
|
||||||
|
return ref
|
||||||
|
return fallback
|
||||||
|
|
||||||
|
|
||||||
def role_required(*roles):
|
def role_required(*roles):
|
||||||
"""Require the current user to hold one of the given roles."""
|
"""Require the current user to hold one of the given roles."""
|
||||||
def decorator(fn):
|
def decorator(fn):
|
||||||
|
|||||||
Reference in New Issue
Block a user