This commit is contained in:
copyrighttxt
2026-07-06 13:28:00 -04:00
commit 05211f73ef
441 changed files with 94128 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
{% extends '__layout__.html' %}
{% block title %}Change Email{% endblock %}
{% block head %}
<link href="/static/css/settings.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<style>
.text-secondary {
color: rgb(200,200,200) !important;
}
</style>
{% endblock %}
{% block content %}
<div id="main">
<div class="settings-container">
<h1>Change Email</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div>
{% for category, message in messages %}
{% if category == 'error': %}
<div class="alert border p-2 text-center alert-danger border-danger">
{{ message }}
</div>
{% endif %}
{% if category == 'success': %}
<div class="alert border p-2 text-center alert-success border-success">
{{ message }}
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% endwith %}
<form method="post">
<div class="form-floating mt-2">
<input type="email" class="form-control" id="new-email-input" name="new-email" placeholder="New Email" required>
<label for="floatingPassword">New Email</label>
</div>
<div class="form-floating mt-1">
<input type="password" class="form-control" id="password-input" name="password" placeholder="Password" required>
<label for="floatingPassword">Password</label>
</div>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="d-flex w-100 align-items-center">
<div class="cf-turnstile d-table mt-2" style="margin: 0 0;" data-theme="dark" data-sitekey="{{turnstilekey}}"></div>
<div class="ms-2 flex-grow-1 text-center mt-2">
<p class="text-secondary" style="font-size: 14px;">
Please complete the captcha to change your email.
</p>
<button type="submit" class="btn btn-primary w-100 text-center btn-sm">Update Email</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
+52
View File
@@ -0,0 +1,52 @@
{% extends '__layout__.html' %}
{% block title %}Change Password{% endblock %}
{% block head %}
<link href="/static/css/settings.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
{% endblock %}
{% block content %}
<div id="main">
<div class="settings-container">
<h1>Change Password</h1>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div>
{% for message in messages %}
<div class="alert border border-danger p-2 text-center messagealerts">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<form method="post">
<div class="row">
<div class="col{% if is2FAEnabled: %} pe-1{% endif %}">
<div class="form-floating">
<input type="password" class="form-control" id="current-password-input" name="current-password" placeholder="Current Password" required>
<label for="floatingPassword">Current Password</label>
</div>
</div>
{% if is2FAEnabled: %}
<div class="col ps-1">
<div class="form-floating">
<input type="text" class="form-control" id="2fa-code-input" name="2fa-code" placeholder="2FA Code" required>
<label for="floatingPassword">2FA Code</label>
</div>
</div>
{% endif %}
</div>
<div class="form-floating mt-2">
<input type="password" class="form-control" id="new-password-input" name="new-password" placeholder="New Password" required>
<label for="floatingPassword">New Password</label>
</div>
<p class="text-secondary m-0" style="font-size: 12px;">Make sure to choose a strong password and always use a unique password for every site.</p>
<div class="form-floating mt-1">
<input type="password" class="form-control" id="confirm-password-input" name="confirm-password" placeholder="Confirm Password" required>
<label for="floatingPassword">Confirm Password</label>
</div>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="btn btn-primary mt-2 w-100 text-center">Update Password</button>
</form>
</div>
</div>
{% endblock %}
+35
View File
@@ -0,0 +1,35 @@
{% extends '__layout__.html' %}
{% block title %}Change Username{% endblock %}
{% block head %}
<link href="/static/css/settings.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
{% endblock %}
{% block content %}
<div id="main">
<div class="settings-container">
<h1>Change Username</h1>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div>
{% for message in messages %}
<div class="alert border border-danger p-2 text-center messagealerts">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<form method="post">
<div class="form-floating mt-2">
<input type="text" class="form-control" id="new-username-input" name="new-username" placeholder="New Username" required>
<label for="floatingPassword">New Username</label>
</div>
<div class="form-floating mt-1">
<input type="password" class="form-control" id="password-input" name="password" placeholder="Password" required>
<label for="floatingPassword">Password</label>
</div>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="btn btn-primary mt-2 w-100 text-center">Change Username ( R$ 1000 )</button>
</form>
</div>
</div>
{% endblock %}
@@ -0,0 +1,21 @@
{% extends '__layout__.html' %}
{% block title %}Email Verified{% endblock %}
{% block head %}
<link href="/static/css/settings.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
<style>
.text-secondary {
color: rgb(200,200,200) !important;
}
</style>
{% endblock %}
{% block content %}
<div id="main">
<div class="settings-container p-3 d-flex align-items-center" style="background-color: rgb(26, 26, 26);box-shadow: 0px 0px 10px 0px rgb(0,0,0,0.5);">
<img src="/Thumbs/Head.ashx?x=100&y=100&userId={{UserObj.id}}" style="aspect-ratio: 1/1;" class="rounded border" width="100px">
<div class="ms-3">
<h1>Email Verified!</h1>
<p class="m-0 text-secondary">Thank you for verifying your email!{%if RewardObj: %} As a reward you have been given a <a href="/catalog/{{RewardObj.id}}/" class="text-decoration-none">special item</a>{%endif%}</p>
</div>
</div>
</div>
{% endblock %}
+34
View File
@@ -0,0 +1,34 @@
{% extends '__layout__.html' %}
{% block title %}Enable TOTP{% endblock %}
{% block head %}
<link href="/static/css/settings.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
{% endblock %}
{% block content %}
<div id="main">
<div class="settings-container" style="max-width:350px !important;min-width: 350px !important;">
<h1>Enable TOTP</h1>
<div class="linebreak"></div>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div>
{% for message in messages %}
<div class="alert border border-danger p-2 text-center messagealerts">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<p class="text-white">Scan the QR code below using a authenticator app (eg. Google Authenticator, Authy)</p>
<div class="d-flex align-items-center mb-2">
<img src="/settings/TOTP/image" class="rounded overflow-hidden d-flex" alt="QR Code" style="width:100%;">
</div>
<p class="m-0">Note: Once this is enabled, you <b>CANNOT</b> disable it ever.</p>
<form action="/settings/enableTOTP" method="POST" class="">
<input type="text" name="code" class="form-control w-100" placeholder="123456 ( Enter code from Authenticator )" required>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button class="btn btn-sm fw-bold mt-2 btn-primary w-100">Enable Authenticator</button>
</form>
</div>
</div>
{% endblock %}
+52
View File
@@ -0,0 +1,52 @@
{% extends '__layout__.html' %}
{% block title %}Settings{% endblock %}
{% block head %}
<link href="/static/css/settings.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
{% endblock %}
{% block content %}
<div id="main">
<div class="settings-container">
<h1>Account Settings</h1>
<div class="linebreak"></div>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div>
{% for message in messages %}
<div class="alert border border-danger p-2 text-center messagealerts">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<div class="d-flex align-items-center mb-2">
<p style="width: fit-content;height: 100%;" class="me-auto">Username: <b>{{currentuser.username}}</b></p>
<a class="btn btn-primary btn-sm d-flex fw-bold text-decoration-none" href="/settings/update-username">Edit Username</a>
</div>
<div class="d-flex align-items-center mb-2">
<p style="width: fit-content;height: 100%;" class="me-auto">Password: <b>************</b></p>
<a class="btn btn-primary btn-sm d-flex fw-bold text-decoration-none" href="/settings/update-password">Edit Password</a>
</div>
<div class="d-flex align-items-center mb-2">
<p style="width: fit-content;height: 100%;" class="me-auto">Email: {% if LinkedEmailObj %}<b>{{HiddenEmail}}</b>{%if LinkedEmailObj.verified %}<span class="text-success"> Verified</span>{%else%}<span class="text-warning"> Unverified</span>{%endif%}{%else%}<b>No Email Linked</b>{%endif%}</p>
<a class="btn btn-primary btn-sm d-flex fw-bold text-decoration-none" href="/settings/update-email">{% if LinkedEmailObj %}Edit Email{%else%}Add email{%endif%}</a>
</div>
<div class="d-flex align-items-center mb-2">
<p style="width: fit-content;height: 100%;" class="me-auto">Authenticator App Authentication: {% if TOTPenabled: %}<b>Enabled</b>{% else %}<b>Disabled</b>{% endif %}</p>
{% if not TOTPenabled: %}<a href="/settings/enableTOTP" class="text-decoration-none"><button class="btn btn-success btn-sm d-flex fw-bold">Enable Authenticator</button></a>{% endif %}
</div>
<div class="d-flex align-items-center mb-2">
<p style="width: fit-content;height: 100%;" class="me-auto">Discord Account: {% if DiscordUserInfoObj != None: %}<img src="{{DiscordUserInfoObj.GetAvatarURL()}}" height="30px" class="border rounded-3" style="aspect-ratio: 1/1;"><b class="ms-2">{{DiscordUserInfoObj.Username}}#{{DiscordUserInfoObj.Discriminator}}</b><span> ({{DiscordUserInfoObj.UserId}})</span>{%else%}<b>Not Linked</b>{%endif%}</p>
{% if DiscordUserInfoObj == None: %}<a href="/settings/discord_link" class="text-decoration-none"><button class="btn btn-success btn-sm d-flex fw-bold">Link Discord</button></a>{%else%}<form action="/settings/unlink_discord" method="post"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/><button class="btn btn-danger btn-sm d-flex fw-bold">Unlink Discord</button></form>{% endif %}
</div>
<h5>Description</h5>
<form action="/settings/description" method="POST">
<textarea class="form-control" id="description" rows="3" maxlength="256" name="description" placeholder="Enter a description about yourself here... ( 256 chars max )">{{description}}</textarea>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-primary mt-2">Save</button>
</div>
</form>
</div>
</div>
{% endblock %}
+556
View File
@@ -0,0 +1,556 @@
import base64
import hashlib
import pyotp
import random
import re
import logging
import requests
import uuid
import json
from app.util.discord import DiscordUserInfo, ExchangeCodeForToken, UnexpectedStatusCode, MissingScope, GetUserInfoFromToken, GenerateAuthorizationURL
from app.models.linked_discord import LinkedDiscord
from app.models.user_email import UserEmail
from app.pages.signup.signup import isUsernameAllowed
from app.models.past_usernames import PastUsername
from app.util.redislock import acquire_lock, release_lock
from app.models.usereconomy import UserEconomy
from datetime import datetime, timedelta
from app.pages.messages.messages import CreateSystemMessage
from app.util.membership import GetUserMembership, GiveUserMembership, UserHasHigherMembershipException, UserDoesNotExistException, RemoveUserMembership
from app.enums.MembershipType import MembershipType
from flask import Blueprint, render_template, request, redirect, url_for, session, flash, redirect, make_response, abort
from app.util import auth, websiteFeatures, textfilter, turnstile, transactions
from app.extensions import db, limiter, redis_controller
from app.models.user import User
from requests.auth import HTTPBasicAuth
from config import Config
from app.models.asset import Asset
from app.models.userassets import UserAsset
from sqlalchemy import func
from app.services import economy
from app.enums.TransactionType import TransactionType
config = Config()
def generate_secret_key_from_string(secret_string):
secret_bytes = secret_string.encode('utf-8')
char_list = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
random.seed(secret_bytes)
secret_key = ''.join([random.choice(char_list) for _ in range(32)])
return secret_key
settings = Blueprint("settings", __name__, template_folder="pages")
@settings.route("/settings", methods=["GET"])
@auth.authenticated_required
def settings_page():
user : User = auth.GetCurrentUser()
LinkedDiscordObj : LinkedDiscord = LinkedDiscord.query.filter_by(user_id=user.id).first()
if LinkedDiscordObj is not None:
DiscordUserInfoObj : DiscordUserInfo = DiscordUserInfo(LinkedDiscordObj.discord_id, LinkedDiscordObj.discord_username, LinkedDiscordObj.discord_avatar, GlobalName = None, Discriminator = LinkedDiscordObj.discord_discriminator)
else:
DiscordUserInfoObj = None
LinkedEmailObj : UserEmail = UserEmail.query.filter_by(user_id=user.id).first()
HiddenEmail = None
if LinkedEmailObj:
emailParts = LinkedEmailObj.email.split("@")
FirstPart = emailParts[0][0] + "*" * (len(emailParts[0])-1)
SecondPart = emailParts[1]
HiddenEmail = FirstPart + "@" + SecondPart
return render_template("settings/settings.html", TOTPenabled = user.TOTPEnabled, description=user.description, DiscordUserInfoObj=DiscordUserInfoObj, LinkedEmailObj=LinkedEmailObj, HiddenEmail=HiddenEmail)
@settings.route("/settings/description", methods=["POST"])
@auth.authenticated_required
def settings_description():
description = request.form.get("description")
if description is None:
return redirect("/settings")
if len(description) > 256:
flash("Description is too long")
return redirect("/settings")
newlineCount = description.count("\n")
if newlineCount > 5:
flash("Description can only have 5 lines")
return redirect("/settings")
user : User = auth.GetCurrentUser()
user.description = textfilter.FilterText(description)
db.session.commit()
return redirect("/settings")
@settings.route("/settings/enableTOTP", methods=["GET"])
@auth.authenticated_required
def settings_enableTOTP():
return render_template("settings/enableTOTP.html")
@settings.route("/settings/enableTOTP", methods=["POST"])
@auth.authenticated_required
def settings_enableTOTP_post():
if "code" not in request.form:
flash("Please fill in all fields")
return redirect("/settings/enableTOTP")
code = request.form.get("code")
user : User = auth.GetCurrentUser()
if user.TOTPEnabled:
return redirect("/settings")
secret_key = generate_secret_key_from_string(str(user.id) + config.FLASK_SESSION_KEY)
totp = pyotp.TOTP(secret_key)
if not totp.verify(code):
flash("Invalid code")
return redirect("/settings/enableTOTP")
user.TOTPEnabled = True
db.session.commit()
return redirect("/settings")
@settings.route("/settings/TOTP/image", methods=["GET"])
@auth.authenticated_required
def settings_TOTP_image():
user : User = auth.GetCurrentUser()
if user.TOTPEnabled:
return redirect("/settings")
secret_key = generate_secret_key_from_string(str(user.id) + config.FLASK_SESSION_KEY)
import pyqrcode
totp = pyotp.TOTP(secret_key)
url = pyqrcode.create(totp.provisioning_uri(user.username, issuer_name="Vortexi"))
resp = make_response(base64.b64decode(url.png_as_base64_str(scale=5)))
resp.headers['Content-Type'] = 'image/png'
return resp
import hashlib
def GenerateStateHash( Cookie : str ) -> str:
"""
GenerateStateHash generates a state hash for discord oauth2.
"""
salted_cookie = Cookie + config.FLASK_SESSION_KEY
return hashlib.md5(salted_cookie.encode('utf-8')).hexdigest()
@settings.route("/settings/unlink_discord", methods=["POST"])
@auth.authenticated_required
def settings_unlink_discord():
AuthenticatedUser : User = auth.GetCurrentUser()
LinkedDiscordObj : LinkedDiscord = LinkedDiscord.query.filter_by(user_id=AuthenticatedUser.id).first()
if LinkedDiscordObj is None:
flash("You do not have a linked discord account")
return redirect("/settings")
if LinkedDiscordObj.linked_on + timedelta(hours=24) > datetime.utcnow():
flash("You cannot unlink your discord account until 24 hours after linking")
return redirect("/settings")
db.session.delete(LinkedDiscordObj)
db.session.commit()
UserMembership : MembershipType = GetUserMembership(AuthenticatedUser)
if UserMembership == MembershipType.BuildersClub:
RemoveUserMembership(AuthenticatedUser)
flash("Discord account unlinked")
return redirect("/settings")
@settings.route("/settings/discord_link", methods=["GET"])
@auth.authenticated_required
def settings_discord_link():
if not websiteFeatures.GetWebsiteFeature("UsernameChange"):
flash("Discord Linking is temporarily disabled")
return redirect("/settings")
UserObj : User = auth.GetCurrentUser()
LinkedDiscordObj : LinkedDiscord = LinkedDiscord.query.filter_by(user_id=UserObj.id).first()
if LinkedDiscordObj is not None:
return redirect("/settings")
state = GenerateStateHash(request.cookies[".ROBLOSECURITY"])
AuthorizationURL = GenerateAuthorizationURL(state)
return redirect(AuthorizationURL)
@settings.route("/settings/discord_handler", methods=["GET"])
@auth.authenticated_required
def settings_discord_handler():
if not websiteFeatures.GetWebsiteFeature("UsernameChange"):
flash("Discord Linking is temporarily disabled")
return redirect("/settings")
UserObj : User = auth.GetCurrentUser()
LinkedDiscordObj : LinkedDiscord = LinkedDiscord.query.filter_by(user_id=UserObj.id).first()
if LinkedDiscordObj is not None:
flash("You already have a linked discord account")
return redirect("/settings")
expectedState = GenerateStateHash(request.cookies[".ROBLOSECURITY"])
givenState = request.args.get(key="state", default=None, type=str)
if givenState is None:
flash("Invalid state")
return redirect("/settings")
if givenState != expectedState:
flash("Invalid state")
return redirect("/settings")
givenCode = request.args.get(key="code", default=None, type=str)
if givenCode is None:
flash("Invalid code")
return redirect("/settings")
try:
DiscordOAuth2TokenExchangeResponseJSON = ExchangeCodeForToken(givenCode)
except UnexpectedStatusCode as e:
flash(f"UnexpectedStatusCodeException: {str(e)}")
return redirect("/settings")
except MissingScope as e:
flash(f"MissingScopeException: {str(e)}")
return redirect("/settings")
try:
DiscordUserInfoObj : DiscordUserInfo = GetUserInfoFromToken(DiscordOAuth2TokenExchangeResponseJSON["access_token"])
except UnexpectedStatusCode as e:
flash(f"UnexpectedStatusCodeException: {str(e)}")
return redirect("/settings")
DiscordAccountCreationDatetime = datetime.utcfromtimestamp( ( (int(DiscordUserInfoObj.UserId) >> 22) + 1420070400000) / 1000 )
# if DiscordAccountCreationDatetime + timedelta(days=7) > datetime.utcnow():
# flash("Discord account must be at least 7 days old")
# return redirect("/settings")
LinkedDiscordObj : LinkedDiscord = LinkedDiscord.query.filter_by(discord_id=DiscordUserInfoObj.UserId).first()
if LinkedDiscordObj is not None:
if LinkedDiscordObj.linked_on + timedelta(hours=24) > datetime.utcnow():
flash("This discord account has recently been linked to another account, please try again later")
return redirect("/settings")
try:
CreateSystemMessage(
subject = "Discord Account unlinked",
message = f"Your discord account was unlinked from your account because \"Account linked to another user\", if you currently have a Builders Club membership it will be automatically removed from your account until you link your discord account again.",
userid = LinkedDiscordObj.user_id
)
OldUserMembership : MembershipType = GetUserMembership(LinkedDiscordObj.user_id)
if OldUserMembership == MembershipType.BuildersClub:
RemoveUserMembership(LinkedDiscordObj.user_id)
except:
pass
db.session.delete(LinkedDiscordObj)
db.session.commit()
NewLinkedDiscordObj = LinkedDiscord(
user_id=UserObj.id,
discord_id=DiscordUserInfoObj.UserId,
discord_username=DiscordUserInfoObj.Username,
discord_discriminator=DiscordUserInfoObj.Discriminator,
discord_avatar=DiscordUserInfoObj.AvatarHash,
discord_access_token=DiscordOAuth2TokenExchangeResponseJSON["access_token"],
discord_refresh_token=DiscordOAuth2TokenExchangeResponseJSON["refresh_token"],
discord_expiry=datetime.utcnow() + timedelta(seconds=DiscordOAuth2TokenExchangeResponseJSON["expires_in"])
)
db.session.add(NewLinkedDiscordObj)
db.session.commit()
CurrentUserMembership : MembershipType = GetUserMembership(UserObj)
if CurrentUserMembership == MembershipType.NonBuildersClub:
try:
GiveUserMembership(UserObj, MembershipType.BuildersClub, expiration=timedelta(days=31))
except Exception as e:
logging.error(f"Failed to give user builders club membership: {str(e)}")
return redirect("/settings")
return redirect("/settings")
@settings.route("/settings/update-password", methods=["GET"])
@auth.authenticated_required
def settings_update_password():
if not websiteFeatures.GetWebsiteFeature("PasswordChange"):
flash("Password changing is temporarily disabled")
return redirect("/settings")
AuthenticatedUser : User = auth.GetCurrentUser()
return render_template("settings/changepassword.html", is2FAEnabled = AuthenticatedUser.TOTPEnabled)
@settings.route("/settings/update-password", methods=["POST"])
@auth.authenticated_required
@limiter.limit("20/minute")
def settings_update_password_post():
if not websiteFeatures.GetWebsiteFeature("PasswordChange"):
flash("Password changing is temporarily disabled")
return redirect("/settings")
AuthenticatedUser : User = auth.GetCurrentUser()
CurrentPassword = request.form.get( key = "current-password", default = "", type = str )
NewPassword = request.form.get( key = "new-password", default = "", type = str )
NewPasswordConfirm = request.form.get( key = "confirm-password", default = "", type = str )
OTPCode = request.form.get( key = "2fa-code", default = None, type = str )
if AuthenticatedUser.TOTPEnabled:
if OTPCode is None:
flash("Please fill in all fields")
return redirect("/settings/update-password")
isValidCode = auth.Validate2FACode(AuthenticatedUser.id, OTPCode)
if not isValidCode:
flash("Invalid 2FA code")
return redirect("/settings/update-password")
if not auth.VerifyPassword(AuthenticatedUser, CurrentPassword):
flash("Invalid current password")
return redirect("/settings/update-password")
if NewPassword != NewPasswordConfirm:
flash("Passwords do not match")
return redirect("/settings/update-password")
if len(NewPassword) < 8:
flash("Password must be at least 8 characters")
return redirect("/settings/update-password")
if len(NewPassword) > 256:
flash("Password cannot be longer than 256 characters")
return redirect("/settings/update-password")
auth.SetPassword(AuthenticatedUser, NewPassword)
flash("Password updated")
return redirect("/settings")
@settings.route("/settings/update-username", methods=["GET"])
@auth.authenticated_required
def settings_update_username():
if not websiteFeatures.GetWebsiteFeature("UsernameChange"):
flash("Username changing is temporarily disabled")
return redirect("/settings")
return render_template("settings/changeusername.html")
@settings.route("/settings/update-username", methods=["POST"])
@auth.authenticated_required
@limiter.limit("20/minute")
def settings_update_username_post():
if not websiteFeatures.GetWebsiteFeature("UsernameChange"):
flash("Username changing is temporarily disabled")
return redirect("/settings")
AuthenticatedUser : User = auth.GetCurrentUser()
NewUsername = request.form.get( key = "new-username", default = "", type = str )
Password = request.form.get( key = "password", default = "", type = str )
if not auth.VerifyPassword(AuthenticatedUser, Password):
flash("Incorrect password")
return redirect("/settings/update-password")
isValidUsername , Reason = isUsernameAllowed(NewUsername)
if not isValidUsername:
flash(Reason)
return redirect("/settings/update-username")
try:
textfilter.FilterText(NewUsername, ThrowException=True, UseExtendedBadWords=True)
except Exception as e:
flash("Username contains inappropriate words")
return redirect("/settings/update-username")
if NewUsername == AuthenticatedUser.username:
flash("Username cannot be the same as your current username")
return redirect("/settings/update-username")
UsernameTaken = User.query.filter(func.lower(User.username) == func.lower(NewUsername)).first()
if UsernameTaken is not None:
flash("Username is taken")
return redirect("/settings/update-username")
PastUsernameObj : PastUsername = PastUsername.query.filter(func.lower(PastUsername.username) == func.lower(NewUsername)).first()
if PastUsernameObj is not None:
if PastUsernameObj.user_id != AuthenticatedUser.id:
flash("Username is taken")
return redirect("/settings/update-username")
UserRobuxBalance, _ = economy.GetUserBalance( TargetUser = AuthenticatedUser )
if UserRobuxBalance < 1000:
flash("You need at least 1000 robux to change your username")
return redirect("/settings/update-username")
try:
economy.DecrementTargetBalance(
Target = AuthenticatedUser,
Amount = 1000,
CurrencyType = 0
)
except economy.InsufficientFundsException:
flash("You need at least 1000 robux to change your username")
return redirect("/settings/update-username")
except economy.EconomyLockAcquireException:
flash("Failed to change username, please try again later no robux was deducted")
return redirect("/settings/update-username")
except Exception as e:
flash("Failed to change username, please try again later")
return redirect("/settings/update-username")
try:
NewPastUsernameObj = PastUsername(
user_id=AuthenticatedUser.id,
username=AuthenticatedUser.username
)
db.session.add(NewPastUsernameObj)
AuthenticatedUser.username = NewUsername
db.session.commit()
except Exception as e:
flash("Failed to change username, please try again later")
return redirect("/settings/update-username")
try:
transactions.CreateTransaction(
CurrencyAmount = 1000,
CurrencyType = 0,
TransactionType = TransactionType.Purchase,
AssetId = None,
CustomText = None,
Sender = AuthenticatedUser
)
except:
pass
flash("Username updated")
return redirect("/settings")
@settings.route("/settings/update-email", methods=["GET"])
@auth.authenticated_required
def settings_update_email():
if not websiteFeatures.GetWebsiteFeature("EmailChange"):
flash("Email changing is temporarily disabled")
return redirect("/settings")
return render_template("settings/changeemail.html")
EmailRegex = r"^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$" # RFC 5322 Compliant, because some fucker is gonna complain that he cant verify his email from his self-hosted shit
@settings.route("/settings/update-email", methods=["POST"])
@auth.authenticated_required
@limiter.limit("20/minute")
def settings_update_email_post():
AuthenticatedUser : User = auth.GetCurrentUser()
# We've been breached. Allow people to change their emails without cooldown
#if redis_controller.exists(f"update_email_v1_cooldown:{AuthenticatedUser.id}"):
#flash("Please wait 12 hours before updating your email again", "error")
#return redirect("/settings/update-email")
NewEmail = request.form.get( key = "new-email", default = "", type = str ).strip().lower()
Password = request.form.get( key = "password", default = "", type = str )
CloudflareTurnstileKey = request.form.get( key = "cf-turnstile-response", default = None, type=str )
if CloudflareTurnstileKey is None or CloudflareTurnstileKey == '':
flash("Please complete the captcha", "error")
return redirect("/settings/update-password")
if not turnstile.VerifyToken( CloudflareTurnstileKey ):
flash("Invalid captcha answer", "error")
return redirect("/settings/update-password")
if not auth.VerifyPassword(AuthenticatedUser, Password):
flash("Incorrect password", "error")
return redirect("/settings/update-password")
if not re.match( EmailRegex, NewEmail ):
flash("Invalid Email, Not RFC 5322 compliant", "error")
return redirect("/settings/update-password")
UserEmailObj : UserEmail = UserEmail.query.filter_by(user_id = AuthenticatedUser.id ).first()
if UserEmailObj:
if UserEmailObj.email == NewEmail and UserEmailObj.verified:
flash("Email is already verified to this account", "error")
return redirect("/settings/update-email")
UserEmailObj.email = NewEmail
UserEmailObj.verified = False
UserEmailObj.updated_at = datetime.utcnow()
else:
NewUserEmailObj = UserEmail(
user_id = AuthenticatedUser.id,
email = NewEmail,
verified = False
)
db.session.add(NewUserEmailObj)
db.session.commit()
VerificationUUID = str(uuid.uuid4())
VerificationURL = f"{config.BaseURL}/settings/update-email/verify?id={VerificationUUID}&user={AuthenticatedUser.id}"
redis_controller.set(f"update_email_v1:{VerificationUUID}", AuthenticatedUser.id, ex=60 * 60 * 2)
redis_controller.set(f"update_email_v1_cooldown:{AuthenticatedUser.id}", "1", ex=60 * 60 * 12)
# Send verification email
EmailData = {
"Messages": [
{
"From": {
"Email": Config.MAILJET_NOREPLY_SENDER,
"Name": "vortexi.cc"
},
"To": [
{
"Email": NewEmail,
"Name": AuthenticatedUser.username
}
],
"TemplateID": Config.MAILJET_EMAILVERIFY_TEMPLATE_ID,
"TemplateLanguage": True,
"Subject": f"Verify your email on Vortexi! - {AuthenticatedUser.username}",
"Variables": {
"username": AuthenticatedUser.username,
"verificationlink": VerificationURL
}
}
]
}
try:
EmailResponse = requests.post(
url="https://api.mailjet.com/v3.1/send",
data=json.dumps(EmailData),
headers={
"Content-Type": "application/json"
},
auth = HTTPBasicAuth(
Config.MAILJET_APIKEY,
Config.MAILJET_SECRETKEY
)
)
if EmailResponse.status_code != 200:
flash("Failed to send verification email", "error")
redis_controller.delete(f"update_email_v1:{VerificationUUID}")
redis_controller.delete(f"update_email_v1_cooldown:{AuthenticatedUser.id}")
return redirect("/settings/update-email")
except Exception as e:
flash("Failed to send verification email", "error")
redis_controller.delete(f"update_email_v1:{VerificationUUID}")
redis_controller.delete(f"update_email_v1_cooldown:{AuthenticatedUser.id}")
return redirect("/settings/update-email")
flash("Verification email sent", "success")
return redirect("/settings/update-email")
@settings.route("/settings/update-email/verify", methods=["GET"])
def VerifyEmailGet():
EmailVerificationUUID : str = request.args.get( key = "id", default = "", type = str )
ExpectedUserId : int = request.args.get( key = "user", default = None, type = int )
if EmailVerificationUUID == "" or ExpectedUserId is None:
return abort(404)
UserId = redis_controller.get(f"update_email_v1:{EmailVerificationUUID}")
if UserId is None:
return abort(404)
if int(UserId) != ExpectedUserId:
return abort(404)
UserObj : User = User.query.filter_by(id=UserId).first()
UserEmailObj : UserEmail = UserEmail.query.filter_by(user_id=UserObj.id).first()
if UserEmailObj is None:
return abort(404)
UserEmailObj.verified = True
UserEmailObj.updated_at = datetime.utcnow()
db.session.commit()
RewardObj = None
if config.VERIFIED_EMAIL_REWARD_ASSET > 0:
if not UserAsset.query.filter_by(userid=UserObj.id, assetid=config.VERIFIED_EMAIL_REWARD_ASSET).first():
NewUserAssetObj : UserAsset = UserAsset(
userid=UserObj.id,
assetid=config.VERIFIED_EMAIL_REWARD_ASSET
)
db.session.add(NewUserAssetObj)
db.session.commit()
RewardObj = Asset.query.filter_by(id=config.VERIFIED_EMAIL_REWARD_ASSET).first()
return render_template("settings/emailverify_success.html", UserObj = UserObj, RewardObj = RewardObj)