add web
This commit is contained in:
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user