added discord oauth2 to get basic user info to complete profile when registering
This commit is contained in:
@@ -17,6 +17,12 @@ FLASK_DEBUG=true
|
||||
# Dicord_User_ID in the db / remove if you don't want discord notifs.
|
||||
DISCORD_BOT_TOKEN=my_discord_bot_token
|
||||
|
||||
# Discord OAuth2 Configuration (for "Connect Discord" on sign-up page)
|
||||
# Create an application at https://discord.com/developers/applications
|
||||
DISCORD_CLIENT_ID=
|
||||
DISCORD_CLIENT_SECRET=
|
||||
DISCORD_REDIRECT_URI=http://localhost:5000/auth/discord/callback
|
||||
|
||||
#where to find the db (hosted on render for now)
|
||||
DATABASE_URL=URI_vers_db_posgres
|
||||
|
||||
|
||||
+1
-1
@@ -151,7 +151,7 @@ def create_app():
|
||||
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
|
||||
"style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net; "
|
||||
"font-src 'self' https://cdnjs.cloudflare.com; "
|
||||
"img-src 'self' data:; "
|
||||
"img-src 'self' data: https://cdn.discordapp.com; "
|
||||
"connect-src 'self'; "
|
||||
"frame-ancestors 'none'; "
|
||||
"base-uri 'self'; "
|
||||
|
||||
@@ -6,6 +6,7 @@ password policy enforcement and CAPTCHA verification.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, session
|
||||
from flask_login import login_user, logout_user, login_required, current_user
|
||||
@@ -14,11 +15,27 @@ from app.models import User, Player, ESPORT_GAMES
|
||||
from app.validators import RegisterSchema, LoginSchema
|
||||
from marshmallow import ValidationError
|
||||
from urllib.parse import urlparse
|
||||
import requests
|
||||
|
||||
# Account lockout settings
|
||||
MAX_LOGIN_ATTEMPTS = 5
|
||||
LOCKOUT_DURATION_MINUTES = 15
|
||||
|
||||
# Discord OAuth2 configuration
|
||||
DISCORD_CLIENT_ID = os.getenv('DISCORD_CLIENT_ID')
|
||||
DISCORD_CLIENT_SECRET = os.getenv('DISCORD_CLIENT_SECRET')
|
||||
DISCORD_REDIRECT_URI = os.getenv('DISCORD_REDIRECT_URI')
|
||||
DISCORD_API_BASE = 'https://discord.com/api/v10'
|
||||
|
||||
# Mapping from Discord connection platform to E-Sports games
|
||||
DISCORD_PLATFORM_TO_GAMES = {
|
||||
'steam': ['Counter-Strike 2'],
|
||||
'battlenet': ['Overwatch 2'],
|
||||
'epicgames': ['Rocket League'],
|
||||
'xbox': ['Apex Legends', 'Rainbow Six Siege', 'Rocket League'],
|
||||
'playstation': ['Apex Legends', 'Rainbow Six Siege', 'Rocket League'],
|
||||
}
|
||||
|
||||
|
||||
def is_safe_url(url):
|
||||
"""Validate that a URL is safe for redirection (same origin).
|
||||
@@ -224,6 +241,9 @@ def register():
|
||||
phone = validated.get('phone')
|
||||
selected_games = validated.get('games', [])
|
||||
discord_username = validated.get('discord_username')
|
||||
discord_user_id = validated.get('discord_user_id')
|
||||
trn_username = request.form.get('trn_username', '').strip() or None
|
||||
league_os_profile = validated.get('league_os_profile')
|
||||
|
||||
if User.query.filter_by(username=username).first():
|
||||
flash('Username already exists.', 'danger')
|
||||
@@ -253,6 +273,8 @@ def register():
|
||||
phone=phone,
|
||||
games=','.join(selected_games) if selected_games else None,
|
||||
discord_username=discord_username,
|
||||
discord_user_id=discord_user_id,
|
||||
league_os_profile=league_os_profile,
|
||||
)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
@@ -271,6 +293,9 @@ def register():
|
||||
db.session.add(gamertag)
|
||||
db.session.commit()
|
||||
|
||||
# Clear Discord OAuth data from session after successful registration
|
||||
session.pop('discord_oauth', None)
|
||||
|
||||
flash('Your account has been created! You can now log in.', 'success')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
@@ -279,6 +304,139 @@ def register():
|
||||
return render_template('pages/register.html', esport_games=ESPORT_GAMES, captcha=captcha)
|
||||
|
||||
|
||||
@auth_bp.route('/discord/login')
|
||||
def discord_login():
|
||||
"""Redirect the user to Discord's OAuth2 authorization page.
|
||||
|
||||
Requests the 'identify' and 'connections' scopes so we can retrieve
|
||||
the user's Discord username, ID, and linked gaming accounts.
|
||||
|
||||
Returns:
|
||||
Response: Redirect to Discord authorization URL.
|
||||
"""
|
||||
if not DISCORD_CLIENT_ID:
|
||||
flash('Discord OAuth2 is not configured.', 'danger')
|
||||
return redirect(url_for('auth.register'))
|
||||
|
||||
params = {
|
||||
'client_id': DISCORD_CLIENT_ID,
|
||||
'redirect_uri': DISCORD_REDIRECT_URI,
|
||||
'response_type': 'code',
|
||||
'scope': 'identify connections',
|
||||
}
|
||||
query = '&'.join(f'{k}={requests.utils.quote(v)}' for k, v in params.items())
|
||||
auth_url = f'{DISCORD_API_BASE}/oauth2/authorize?{query}'
|
||||
return redirect(auth_url)
|
||||
|
||||
|
||||
@auth_bp.route('/discord/callback')
|
||||
def discord_callback():
|
||||
"""Handle the OAuth2 callback from Discord.
|
||||
|
||||
Exchanges the authorization code for an access token, then fetches
|
||||
the user's profile (/users/@me) and connections (/users/@me/connections).
|
||||
Results are stored in the session and the user is redirected back to
|
||||
the registration form where fields will be pre-filled.
|
||||
|
||||
Returns:
|
||||
Response: Redirect to registration page.
|
||||
"""
|
||||
code = request.args.get('code')
|
||||
if not code:
|
||||
flash('Discord authorization failed. No code received.', 'danger')
|
||||
return redirect(url_for('auth.register'))
|
||||
|
||||
# Exchange the authorization code for an access token
|
||||
token_data = {
|
||||
'client_id': DISCORD_CLIENT_ID,
|
||||
'client_secret': DISCORD_CLIENT_SECRET,
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': DISCORD_REDIRECT_URI,
|
||||
}
|
||||
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
||||
|
||||
try:
|
||||
token_response = requests.post(
|
||||
f'{DISCORD_API_BASE}/oauth2/token',
|
||||
data=token_data,
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
)
|
||||
token_response.raise_for_status()
|
||||
token_json = token_response.json()
|
||||
access_token = token_json.get('access_token')
|
||||
except requests.RequestException as e:
|
||||
flash(f'Failed to connect to Discord. Please try again.', 'danger')
|
||||
return redirect(url_for('auth.register'))
|
||||
|
||||
if not access_token:
|
||||
flash('Failed to obtain Discord access token.', 'danger')
|
||||
return redirect(url_for('auth.register'))
|
||||
|
||||
auth_headers = {'Authorization': f'Bearer {access_token}'}
|
||||
|
||||
# Fetch the user's Discord profile
|
||||
try:
|
||||
user_response = requests.get(
|
||||
f'{DISCORD_API_BASE}/users/@me',
|
||||
headers=auth_headers,
|
||||
timeout=10,
|
||||
)
|
||||
user_response.raise_for_status()
|
||||
user_data = user_response.json()
|
||||
except requests.RequestException:
|
||||
flash('Failed to fetch Discord user profile.', 'danger')
|
||||
return redirect(url_for('auth.register'))
|
||||
|
||||
# Fetch the user's connected gaming accounts
|
||||
connections = []
|
||||
try:
|
||||
conn_response = requests.get(
|
||||
f'{DISCORD_API_BASE}/users/@me/connections',
|
||||
headers=auth_headers,
|
||||
timeout=10,
|
||||
)
|
||||
conn_response.raise_for_status()
|
||||
connections = conn_response.json()
|
||||
except requests.RequestException:
|
||||
# Non-critical: we can still proceed without connections
|
||||
pass
|
||||
|
||||
# Build gamertag suggestions from Discord connections
|
||||
gamertag_suggestions = {}
|
||||
for conn in connections:
|
||||
platform = conn.get('type', '')
|
||||
name = conn.get('name', '').strip()
|
||||
if not name or platform not in DISCORD_PLATFORM_TO_GAMES:
|
||||
continue
|
||||
for game in DISCORD_PLATFORM_TO_GAMES[platform]:
|
||||
# Only set if not already set (first connection wins)
|
||||
if game not in gamertag_suggestions:
|
||||
gamertag_suggestions[game] = name
|
||||
|
||||
# Build a list of games to auto-select (unambiguous platform mappings)
|
||||
auto_select_games = []
|
||||
for conn in connections:
|
||||
platform = conn.get('type', '')
|
||||
if platform in ('steam', 'battlenet', 'epicgames'):
|
||||
for game in DISCORD_PLATFORM_TO_GAMES[platform]:
|
||||
if game not in auto_select_games:
|
||||
auto_select_games.append(game)
|
||||
|
||||
# Store in session for the registration form to use
|
||||
session['discord_oauth'] = {
|
||||
'id': user_data.get('id'),
|
||||
'username': user_data.get('username'),
|
||||
'avatar': user_data.get('avatar'),
|
||||
'gamertag_suggestions': gamertag_suggestions,
|
||||
'auto_select_games': auto_select_games,
|
||||
}
|
||||
|
||||
flash('Discord account connected! Your profile has been pre-filled.', 'success')
|
||||
return redirect(url_for('auth.register'))
|
||||
|
||||
|
||||
@auth_bp.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
{% block title %}Register - TryoutPro{% endblock %}
|
||||
{% block auth_content %}
|
||||
<form method="POST" action="{{ url_for('auth.register') }}" class="auth-form" id="register-form">
|
||||
<form method="POST" action="{{ url_for('auth.register') }}" class="auth-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div class="form-group">
|
||||
<label for="full_name"><i class="fas fa-id-card"></i> Full Name</label>
|
||||
@@ -22,32 +22,102 @@
|
||||
|
||||
<hr class="section-divider">
|
||||
<h4 class="section-title"><i class="fas fa-gamepad"></i> E-Sports Profile</h4>
|
||||
<p class="text-muted small">Select the games you play and provide your gamertag for each.</p>
|
||||
|
||||
<div class="form-group" id="games-container">
|
||||
<label><i class="fas fa-headset"></i> Games You Play</label>
|
||||
{% for game in esport_games %}
|
||||
<div class="game-entry">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="games" value="{{ game }}" class="game-checkbox" data-game="{{ game }}">
|
||||
<span>{{ game }}</span>
|
||||
</label>
|
||||
<div class="gamertag-input" id="gamertag-group-{{ game | replace(' ', '_') }}" style="display:none; margin-top: 4px; margin-left: 20px;">
|
||||
<input type="text" name="gamertag_{{ game }}" id="gamertag_{{ game | replace(' ', '_') }}"
|
||||
placeholder="Enter your {{ game }} gamertag" class="gamertag-field"
|
||||
data-game="{{ game }}">
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<small class="form-text text-muted">Check each game you play, then enter your gamertag for that game.</small>
|
||||
</div>
|
||||
<p class="text-muted small">Set up your competitive gaming profile for tryouts.</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="discord_username"><i class="fab fa-discord"></i> Discord Username</label>
|
||||
<input type="text" id="discord_username" name="discord_username" placeholder="e.g. YourName#1234">
|
||||
<label><i class="fas fa-headset"></i> Games You Play</label>
|
||||
<div class="checkbox-grid">
|
||||
{% for game in esport_games %}
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="games" value="{{ game }}"
|
||||
{% if session.get('discord_oauth') and game in session['discord_oauth'].get('auto_select_games', []) %}checked{% endif %}
|
||||
onchange="toggleGamertagInput(this)">
|
||||
<span>{{ game }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<small class="form-text text-muted">Select all games you're signing in for.</small>
|
||||
</div>
|
||||
|
||||
<!-- Gamertag fields per game (shown when game is checked or pre-filled from Discord) -->
|
||||
{% set discord_suggestions = session.get('discord_oauth', {}).get('gamertag_suggestions', {}) %}
|
||||
{% for game in esport_games %}
|
||||
<div class="form-group gamertag-group" id="gamertag_group_{{ game|replace(' ', '_') }}"
|
||||
style="{% if session.get('discord_oauth') and game in discord_suggestions %}display:block{% else %}display:none{% endif %}">
|
||||
<label for="gamertag_{{ game }}"><i class="fas fa-gamepad"></i> {{ game }} Gamertag</label>
|
||||
<input type="text" id="gamertag_{{ game }}" name="gamertag_{{ game }}"
|
||||
placeholder="Enter your {{ game }} username"
|
||||
value="{{ discord_suggestions.get(game, '') }}">
|
||||
<small class="form-text text-muted">
|
||||
Your in-game name or username for {{ game }}.
|
||||
{% if game in discord_suggestions %}
|
||||
<span class="text-success">Pre-filled from Discord connections.</span>
|
||||
{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="trn_username"><i class="fas fa-chart-line"></i> TRN (Tracker Network) Username</label>
|
||||
<input type="text" id="trn_username" name="trn_username" placeholder="e.g. YourTrackerGGUsername">
|
||||
<small class="form-text text-muted">Your public Tracker Network profile name. Others can click it to view your stats.</small>
|
||||
</div>
|
||||
|
||||
<!-- Discord Connection Section -->
|
||||
{% set discord_data = session.get('discord_oauth') %}
|
||||
{% if discord_data %}
|
||||
<div class="form-group discord-connected">
|
||||
<label><i class="fab fa-discord"></i> Discord Account</label>
|
||||
<div class="discord-connection-card">
|
||||
<div class="discord-user-display">
|
||||
{% if discord_data.avatar %}
|
||||
{% set avatar_url = 'https://cdn.discordapp.com/avatars/' + discord_data.id|string + '/' + discord_data.avatar + '.png?size=64' %}
|
||||
<img src="{{ avatar_url }}" alt="Discord Avatar" class="discord-avatar" width="48" height="48">
|
||||
{% else %}
|
||||
<div class="discord-avatar-placeholder">
|
||||
<i class="fab fa-discord"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="discord-user-info">
|
||||
<span class="discord-username">{{ discord_data.username }}</span>
|
||||
<span class="discord-connected-label text-success">
|
||||
<i class="fas fa-check-circle"></i> Connected
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ url_for('auth.discord_login') }}" class="btn btn-sm btn-outline discord-reconnect">
|
||||
<i class="fas fa-sync-alt"></i> Reconnect
|
||||
</a>
|
||||
</div>
|
||||
<input type="hidden" name="discord_username" value="{{ discord_data.username }}">
|
||||
<input type="hidden" name="discord_user_id" value="{{ discord_data.id }}">
|
||||
<small class="form-text text-success">
|
||||
<i class="fas fa-check-circle"></i> Your Discord account is connected. Gamertags from your linked game accounts have been pre-filled.
|
||||
</small>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="form-group discord-connect-section">
|
||||
<label><i class="fab fa-discord"></i> Discord Connection</label>
|
||||
<p class="text-muted small">Connect your Discord account to automatically fill your profile and gamertags from your connected game accounts (Steam, Battle.net, Xbox, etc.).</p>
|
||||
<a href="{{ url_for('auth.discord_login') }}" class="btn btn-discord btn-block">
|
||||
<i class="fab fa-discord"></i> Connect Discord Account
|
||||
</a>
|
||||
<small class="form-text text-muted">You can also enter your Discord username manually below.</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="discord_username"><i class="fab fa-discord"></i> Discord Username (Manual)</label>
|
||||
<input type="text" id="discord_username" name="discord_username" placeholder="e.g. YourName">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="league_os_profile"><i class="fas fa-link"></i> League OS Connection</label>
|
||||
<input type="text" id="league_os_profile" name="league_os_profile" placeholder="League OS profile link or ID">
|
||||
<small class="form-text text-muted">Connect your League OS profile for organized play.</small>
|
||||
</div>
|
||||
|
||||
<hr class="section-divider">
|
||||
<h4 class="section-title"><i class="fas fa-shield-alt"></i> Security</h4>
|
||||
<div class="form-group">
|
||||
<label for="password"><i class="fas fa-lock"></i> Password</label>
|
||||
<input type="password" id="password" name="password" placeholder="Create a password" required minlength="6">
|
||||
@@ -58,67 +128,162 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="captcha_answer">
|
||||
<i class="fas fa-shield-alt"></i> Security Check: {{ captcha.question }}
|
||||
</label>
|
||||
<input type="text" id="captcha_answer" name="captcha_answer" placeholder="Enter the answer" required>
|
||||
<label for="captcha_answer"><i class="fas fa-calculator"></i> {{ captcha.question }}</label>
|
||||
<input type="number" id="captcha_answer" name="captcha_answer" placeholder="Answer" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-block">Create Account</button>
|
||||
<p class="auth-link">Already have an account? <a href="{{ url_for('auth.login') }}">Sign in</a></p>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const checkboxes = document.querySelectorAll('.game-checkbox');
|
||||
const form = document.getElementById('register-form');
|
||||
|
||||
checkboxes.forEach(function(checkbox) {
|
||||
checkbox.addEventListener('change', function() {
|
||||
const game = this.dataset.game.replace(/ /g, '_');
|
||||
const gamertagGroup = document.getElementById('gamertag-group-' + game);
|
||||
const gamertagField = document.getElementById('gamertag_' + game);
|
||||
|
||||
if (this.checked) {
|
||||
gamertagGroup.style.display = 'block';
|
||||
gamertagField.setAttribute('required', 'required');
|
||||
} else {
|
||||
gamertagGroup.style.display = 'none';
|
||||
gamertagField.removeAttribute('required');
|
||||
gamertagField.value = '';
|
||||
// Toggle gamertag input visibility when a game checkbox is checked/unchecked
|
||||
function toggleGamertagInput(checkbox) {
|
||||
var game = checkbox.value.replace(/ /g, '_');
|
||||
var group = document.getElementById('gamertag_group_' + game);
|
||||
if (group) {
|
||||
group.style.display = checkbox.checked ? 'block' : 'none';
|
||||
// Clear the input when unchecking (unless it was pre-filled from Discord)
|
||||
if (!checkbox.checked) {
|
||||
var input = group.querySelector('input[type="text"]');
|
||||
if (input && !input.dataset.discordPrefilled) {
|
||||
input.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize on page load (in case of form re-render after validation error)
|
||||
const game = checkbox.dataset.game.replace(/ /g, '_');
|
||||
const gamertagGroup = document.getElementById('gamertag-group-' + game);
|
||||
if (checkbox.checked) {
|
||||
gamertagGroup.style.display = 'block';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
const checkedGames = document.querySelectorAll('.game-checkbox:checked');
|
||||
let allFilled = true;
|
||||
|
||||
checkedGames.forEach(function(checkbox) {
|
||||
const game = checkbox.dataset.game.replace(/ /g, '_');
|
||||
const gamertagField = document.getElementById('gamertag_' + game);
|
||||
if (!gamertagField || !gamertagField.value.trim()) {
|
||||
allFilled = false;
|
||||
gamertagField.style.borderColor = '#dc3545';
|
||||
} else {
|
||||
gamertagField.style.borderColor = '';
|
||||
// On page load, ensure gamertag groups match checkbox state
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var checkboxes = document.querySelectorAll('input[name="games"]');
|
||||
checkboxes.forEach(function(checkbox) {
|
||||
var game = checkbox.value.replace(/ /g, '_');
|
||||
var group = document.getElementById('gamertag_group_' + game);
|
||||
if (group) {
|
||||
group.style.display = checkbox.checked ? 'block' : 'none';
|
||||
// Mark Discord-prefilled inputs so they aren't cleared on uncheck
|
||||
var input = group.querySelector('input[type="text"]');
|
||||
if (input && input.value) {
|
||||
input.dataset.discordPrefilled = 'true';
|
||||
}
|
||||
});
|
||||
|
||||
if (!allFilled) {
|
||||
e.preventDefault();
|
||||
alert('Please enter a gamertag for each game you selected.');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Discord Connection Card */
|
||||
.discord-connection-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #2f3136;
|
||||
border: 1px solid #40444b;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.discord-user-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.discord-avatar {
|
||||
border-radius: 50%;
|
||||
border: 2px solid #5865F2;
|
||||
}
|
||||
.discord-avatar-placeholder {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: #5865F2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
color: #fff;
|
||||
}
|
||||
.discord-user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.discord-username {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
color: #fff;
|
||||
}
|
||||
.discord-connected-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.discord-connect-section {
|
||||
background: #2f3136;
|
||||
border: 1px dashed #5865F2;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
.discord-connect-section label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.btn-discord {
|
||||
background-color: #5865F2;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
transition: background-color 0.2s;
|
||||
width: 100%;
|
||||
}
|
||||
.btn-discord:hover {
|
||||
background-color: #4752C4;
|
||||
color: #fff;
|
||||
}
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
color: #5865F2;
|
||||
border: 1px solid #5865F2;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: background-color 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn-outline:hover {
|
||||
background-color: #5865F2;
|
||||
color: #fff;
|
||||
}
|
||||
.discord-reconnect {
|
||||
font-size: 11px;
|
||||
}
|
||||
.btn-sm {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.gamertag-group {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.gamertag-group label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.text-success {
|
||||
color: #57F287 !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -208,6 +208,20 @@ class RegisterSchema(StripMixin):
|
||||
allow_none=True,
|
||||
load_default=None,
|
||||
)
|
||||
discord_user_id = fields.String(
|
||||
validate=validate_discord_user_id,
|
||||
allow_none=True,
|
||||
load_default=None,
|
||||
)
|
||||
trn_username = fields.String(
|
||||
allow_none=True,
|
||||
load_default=None,
|
||||
)
|
||||
league_os_profile = fields.String(
|
||||
validate=validate.Length(max=256),
|
||||
allow_none=True,
|
||||
load_default=None,
|
||||
)
|
||||
|
||||
@validates_schema
|
||||
def validate_password_match(self, data, **kwargs):
|
||||
|
||||
Reference in New Issue
Block a user