Jubilar Django: borrar el portal antiguo, la web ya la sirve Next

El cutover se hizo hoy: Caddy manda www.nightspire.gg a :3001 (Next), así que
Django se quedó sin dominio y sin uso. Se comprobó antes de borrar que nada
dependía de él: ninguna referencia a :8001 en Caddy, el timer de reconciliación
llama a Next, DJANGO_API_BASE no lo leía ni una línea del código (se quita
también de la unidad de systemd), web-next/public es autónomo (736 ficheros
reales, 0 symlinks) y no hay ni una referencia a /static/. Con Django parado la
web siguió dando 200 en todas las rutas.

Se va: home/, novawow/, forum/, wotlk_db/, frontend/ (las islas React que Next
sustituyó), static/, staticfiles/, manage.py, requirements.txt, db.sqlite3 y el
Docker de Django.

Se quedan docs/ y sql/: no son código Django sino documentación y esquemas, y
sql/forum_schema.sql describe la BD acore_web que Next usa hoy.

La BASE DE DATOS no se toca. django_wow y sus 41 tablas home_* son ahora de
Next, que las consulta con SQL directo. El nombre se queda por historia.

README reescrito: describía cómo montar un Django que ya no existe (venv,
manage.py migrate, gunicorn, Docker).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-15 14:39:19 +00:00
parent 9390eefa93
commit 5fd006aca9
740 changed files with 56 additions and 112924 deletions
View File
-51
View File
@@ -1,51 +0,0 @@
# ac_soap.py
import requests
import base64
from django.conf import settings
def execute_soap_command(command):
"""
Ejecuta un comando en el servidor AzerothCore usando SOAP.
Parameters:
- command (str): El comando a ejecutar, por ejemplo, ".additem 3200"
Returns:
- str: La respuesta del servidor si el comando se ejecuta correctamente.
- None: Si ocurre algún error durante la ejecución.
"""
try:
# Crear la carga SOAP
soap_command = f'''<?xml version="1.0" encoding="utf-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="{settings.AC_SOAP_URN}">
<SOAP-ENV:Body>
<ns1:executeCommand>
<command>{command}</command>
</ns1:executeCommand>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>'''
# Autenticación básica usando las credenciales en settings.py
auth_header = f"Basic {base64.b64encode(f'{settings.AC_SOAP_USER}:{settings.AC_SOAP_PASSWORD}'.encode()).decode()}"
headers = {
'Authorization': auth_header,
'Content-Type': 'application/xml'
}
# Realizar la solicitud SOAP al servidor de AzerothCore
response = requests.post(settings.AC_SOAP_URL, data=soap_command, headers=headers, timeout=5)
# Verificar si la respuesta fue exitosa
if response.status_code == 200:
return response.text
else:
print(f"[ERROR] Comando SOAP fallido: {response.status_code} - {response.text}")
return None
except requests.exceptions.RequestException as e:
print(f"[EXCEPCIÓN] Error de conexión con el servidor SOAP: {str(e)}")
return None
except Exception as e:
print(f"[EXCEPCIÓN] Error inesperado al ejecutar el comando SOAP: {str(e)}")
return None
-204
View File
@@ -1,204 +0,0 @@
# home/admin.py
from django.contrib import admin
from django import forms
from .models import (
Noticia, ClienteCategoria, SistemaOperativo, ServerSelection,
RecruitAFriend, DownloadClientPage, ContentCreator, RecruitReward, GuildRenameSettings, VoteSite, RenamePrice, CustomizePrice, ChangeRacePrice, ChangeFactionPrice, LevelUpPrice, GoldPrice, TransferPrice, MaintenanceMode,
StripeLog, Pedido
)
from django.db import connections
import logging
from django.shortcuts import redirect
from django_ckeditor_5.widgets import CKEditor5Widget
logger = logging.getLogger(__name__)
@admin.register(StripeLog)
class StripeLogAdmin(admin.ModelAdmin):
list_display = (
'account_id', 'username', 'email', 'acore_ip', 'stripe_ip', 'product_name',
'amount', 'session_id', 'mode', 'character_name', 'timestamp'
) # Añadido stripe_ip
list_filter = ('mode', 'timestamp') # Filtros por campo en la parte lateral
search_fields = ('username', 'product_name', 'session_id', 'email', 'stripe_ip') # Ahora incluye 'stripe_ip' en la búsqueda
ordering = ('-timestamp',) # Ordenar por timestamp descendentes
@admin.register(Pedido)
class PedidoLogAdmin(admin.ModelAdmin): # Ahora esta es una clase de admin
list_display = ('transaction_id', 'email_cliente', 'estado', 'monto') # Campos que se mostrarán en el panel
search_fields = ('transaction_id', 'email_cliente') # Habilita búsqueda por ID y email
list_filter = ('estado',) # Filtro por estado de pago
@admin.register(ChangeFactionPrice)
class ChangeFactionPriceAdmin(admin.ModelAdmin):
list_display = ('price',)
@admin.register(MaintenanceMode)
class MaintenanceModeAdmin(admin.ModelAdmin):
list_display = ('is_active',)
@admin.register(GoldPrice)
class GoldPriceAdmin(admin.ModelAdmin):
list_display = ('gold_amount', 'price')
ordering = ('gold_amount',)
@admin.register(TransferPrice)
class TransferPriceAdmin(admin.ModelAdmin):
list_display = ('price',)
@admin.register(LevelUpPrice)
class LevelUpPriceAdmin(admin.ModelAdmin):
list_display = ('price',)
# Formulario para el modelo Noticia
class NoticiaAdminForm(forms.ModelForm):
class Meta:
model = Noticia
fields = ['titulo', 'contenido', 'fecha_fin_evento', 'enlace']
widgets = {
'contenido': CKEditor5Widget(config_name='default'),
}
@admin.register(RenamePrice)
class RenamePriceAdmin(admin.ModelAdmin):
list_display = ("price",)
@admin.register(CustomizePrice)
class CustomizePriceAdmin(admin.ModelAdmin):
list_display = ("price",)
@admin.register(ChangeRacePrice)
class ChangeRacePriceAdmin(admin.ModelAdmin):
list_display = ('price',)
@admin.register(Noticia)
class NoticiaAdmin(admin.ModelAdmin):
form = NoticiaAdminForm
list_display = ['titulo', 'fecha_publicacion']
readonly_fields = ['fecha_publicacion']
fields = ['titulo', 'contenido', 'fecha_fin_evento', 'enlace']
# Formulario para el modelo RecruitAFriend
class RecruitAFriendAdminForm(forms.ModelForm):
content = forms.CharField(widget=CKEditor5Widget(config_name='default'))
class Meta:
model = RecruitAFriend
fields = ['title', 'information', 'content']
@admin.register(RecruitAFriend)
class RecruitAFriendAdmin(admin.ModelAdmin):
form = RecruitAFriendAdminForm
list_display = ['title', 'updated_at']
fields = ['title', 'information', 'content']
# Formulario para el modelo DownloadClientPage
class DownloadClientPageAdminForm(forms.ModelForm):
content = forms.CharField(widget=CKEditor5Widget(config_name='default'))
mac_instructions = forms.CharField(widget=CKEditor5Widget(config_name='default'))
class Meta:
model = DownloadClientPage
fields = ['title', 'content', 'mac_instructions']
@admin.register(DownloadClientPage)
class DownloadClientPageAdmin(admin.ModelAdmin):
form = DownloadClientPageAdminForm
list_display = ['title', 'updated_at']
fields = ['title', 'content', 'mac_instructions']
# Formulario para el modelo ContentCreator
class ContentCreatorAdminForm(forms.ModelForm):
description = forms.CharField(widget=CKEditor5Widget(config_name='default'))
class Meta:
model = ContentCreator
fields = [
'title', 'description', 'content_type', 'subtitle',
'youtube_video_url', 'avatar_image_url',
'youtube_channel_url', 'facebook_page_url'
]
@admin.register(ContentCreator)
class ContentCreatorAdmin(admin.ModelAdmin):
form = ContentCreatorAdminForm
list_display = ['title', 'updated_at']
fields = [
'title', 'description', 'content_type', 'subtitle',
'youtube_video_url', 'avatar_image_url',
'youtube_channel_url', 'facebook_page_url'
]
# Administración para ServerSelection
class ServerSelectionAdmin(admin.ModelAdmin):
list_display = ['name', 'address', 'port', 'gamebuild']
change_list_template = "admin/server_selection.html"
def changelist_view(self, request, extra_context=None):
servers = []
try:
with connections['acore_auth'].cursor() as cursor:
cursor.execute("SELECT id, name, address, port, gamebuild FROM realmlist")
servers = cursor.fetchall()
except Exception as e:
logger.error("Error al obtener servidores: %s", e)
if request.method == "POST" and "server_selection" in request.POST:
selected_server_id = int(request.POST["server_selection"])
selected_server = next((s for s in servers if s[0] == selected_server_id), None)
if selected_server:
ServerSelection.objects.all().delete()
ServerSelection.objects.create(
server_id=selected_server[0],
name=selected_server[1],
address=selected_server[2],
port=selected_server[3],
gamebuild=selected_server[4]
)
return redirect("admin:home_serverselection_changelist")
extra_context = extra_context or {}
extra_context['servers'] = servers
return super().changelist_view(request, extra_context=extra_context)
# Inlines para ClienteCategoria
class SistemaOperativoInline(admin.TabularInline):
model = SistemaOperativo
extra = 1
class ClienteCategoriaAdmin(admin.ModelAdmin):
inlines = [SistemaOperativoInline]
@admin.register(RecruitReward)
class RecruitRewardAdmin(admin.ModelAdmin):
list_display = ('required_friends', 'reward_name', 'item_id', 'item_quantity', 'icon_class')
search_fields = ('reward_name',)
@admin.register(GuildRenameSettings)
class GuildRenameSettingsAdmin(admin.ModelAdmin):
list_display = ('id', 'cost') # Asegúrate de incluir 'id' antes de 'cost'
list_display_links = ('id',) # Permite hacer clic en 'id' para editar
list_editable = ('cost',) # Ahora 'cost' es editable en línea
@admin.register(VoteSite)
class VoteSiteAdmin(admin.ModelAdmin):
list_display = ('name', 'points', 'url', 'image_url')
search_fields = ('name',)
list_editable = ('points',)
list_display_links = ('name',)
# Registro de modelos en el admin
admin.site.register(ClienteCategoria, ClienteCategoriaAdmin)
admin.site.register(SistemaOperativo)
admin.site.register(ServerSelection, ServerSelectionAdmin)
-10
View File
@@ -1,10 +0,0 @@
"""Rutas de la API JSON (islas React). Se montan bajo /api/ fuera de i18n_patterns."""
from django.urls import path
from . import views
urlpatterns = [
path('home/news/', views.home_news_api, name='api_home_news'),
path('home/status/', views.home_status_api, name='api_home_status'),
path('account/select/', views.account_select_api, name='api_account_select'),
path('account/me/', views.account_me_api, name='api_account_me'),
]
-6
View File
@@ -1,6 +0,0 @@
from django.apps import AppConfig
class HomeConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'home'
-216
View File
@@ -1,216 +0,0 @@
"""
Utilidades de autenticación para servidores 3.4.3 (TrinityCore) con cuentas
Battle.net.
Dos algoritmos SRP6 distintos conviven:
* ``battlenet_accounts`` -> SRP6 **v2**: PBKDF2-HMAC-SHA512, N de 2048 bits,
g = 2. El "usuario" SRP es HEX(SHA256(UPPER(email))). Es el que usa el
cliente 3.4.3 para conectarse (login por email).
* ``account`` (cuenta de juego) -> SRP6 **Grunt/SHA1**: el clásico de
AzerothCore/3.3.5a. Cada cuenta bnet tiene 1..N cuentas de juego con
username ``<bnetId>#<index>``.
Referencias del propio source 3.4.3:
src/common/Cryptography/Authentication/SRP6.cpp (CalculateX / CalculateVerifier)
src/server/game/Accounts/BattlenetAccountMgr.cpp (CreateBattlenetAccount)
"""
import hashlib
import os
# ---------------------------------------------------------------------------
# SRP6 v2 (battlenet_accounts) — PBKDF2-HMAC-SHA512
# ---------------------------------------------------------------------------
# BnetSRP6v2Base::N (2048 bits) y g, tal cual en SRP6.cpp
_BNET_N = int(
"AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050"
"A37329CBB4A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50"
"E8083969EDB767B0CF6095179A163AB3661A05FBD5FAAAE82918A9962F0B93B8"
"55F97993EC975EEAA80D740ADBF4FF747359D041D5C33EA71D281E446B14773B"
"CA97B43A23FB801676BD207A436C6481F1D2B9078717461A5B9D32E688F87748"
"544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6"
"AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB6"
"94B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73",
16,
)
_BNET_g = 2
_BNET_SALT_LEN = 32
_BNET_ITERATIONS = 15000 # GetXIterations() para v2
_BNET_SRP_VERSION = 2
_TWO_POW_512 = 1 << 512
# TrinityCore almacena el verifier con BigNumber::ToByteVector(), que por
# defecto es little-endian. Si al comparar contra una cuenta creada por el
# propio servidor (`.bnetaccount create`) el login del CLIENTE fallara, prueba a
# poner esto en False (big-endian). El login del PORTAL funciona en cualquier
# caso porque registra y verifica con el mismo criterio.
_BNET_VERIFIER_LITTLE_ENDIAN = True
def bnet_srp_username(email):
"""SRP username de bnet = HEX(SHA256(UPPER(email))) en mayúsculas."""
email_up = email.strip().upper()
return hashlib.sha256(email_up.encode("utf-8")).hexdigest().upper()
def _bnet_calculate_x(email, password, salt):
"""Replica BnetSRP6v2Base::CalculateX (SRP6.cpp)."""
srp_user = bnet_srp_username(email)
tmp = (srp_user + ":" + password).encode("utf-8")
x_bytes = hashlib.pbkdf2_hmac("sha512", tmp, salt, _BNET_ITERATIONS, dklen=64)
x = int.from_bytes(x_bytes, "big")
if x_bytes[0] & 0x80: # si el bit alto está puesto, resta 2^512
x -= _TWO_POW_512
return x % (_BNET_N - 1)
def _int_to_verifier_bytes(value):
"""Emula BigNumber::ToByteVector(): longitud mínima, endianness configurable."""
length = max(1, (value.bit_length() + 7) // 8)
order = "little" if _BNET_VERIFIER_LITTLE_ENDIAN else "big"
return value.to_bytes(length, order)
def _verifier_bytes_to_int(data):
order = "little" if _BNET_VERIFIER_LITTLE_ENDIAN else "big"
return int.from_bytes(data, order)
def bnet_make_registration(email, password):
"""
Genera (salt, verifier, srp_version) para una nueva cuenta Battle.net.
salt: 32 bytes aleatorios (se guarda tal cual).
verifier: g^x mod N en longitud mínima (como TrinityCore::ToByteVector).
"""
salt = os.urandom(_BNET_SALT_LEN)
x = _bnet_calculate_x(email, password, salt)
v = pow(_BNET_g, x, _BNET_N)
return salt, _int_to_verifier_bytes(v), _BNET_SRP_VERSION
def bnet_verify(email, password, salt, stored_verifier):
"""Comprueba la contraseña de una cuenta Battle.net (login por email)."""
if salt is None or stored_verifier is None:
return False
if isinstance(salt, memoryview):
salt = bytes(salt)
if isinstance(stored_verifier, memoryview):
stored_verifier = bytes(stored_verifier)
x = _bnet_calculate_x(email, password, salt)
v = pow(_BNET_g, x, _BNET_N)
# Comparación numérica (robusta frente a padding).
return v == _verifier_bytes_to_int(stored_verifier)
# ---------------------------------------------------------------------------
# SRP6 Grunt/SHA1 (cuenta de juego `account`) — el clásico de 3.3.5a
# ---------------------------------------------------------------------------
_GRUNT_g = 7
_GRUNT_N = int("894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7", 16)
_GRUNT_SALT_LEN = 32
def _sha1(data):
return hashlib.sha1(data).digest()
def game_calculate_verifier(username, password, salt):
"""
SRP6 Grunt (SHA1) para la tabla `account`. ``username`` y ``password`` se
pasan en mayúsculas (TrinityCore hace Utf8ToUpperOnlyLatin en ambos).
Devuelve el verifier en bytes little-endian (32).
"""
if isinstance(salt, memoryview):
salt = bytes(salt)
h1 = _sha1((username.upper() + ":" + password.upper()).encode("utf-8"))
h2 = _sha1(salt + h1)
h2_int = int.from_bytes(h2, "little")
verifier = pow(_GRUNT_g, h2_int, _GRUNT_N)
return verifier.to_bytes(32, "little")
def game_make_registration(username, password):
"""Genera (salt, verifier) Grunt para una cuenta de juego nueva."""
salt = os.urandom(_GRUNT_SALT_LEN)
verifier = game_calculate_verifier(username, password[:16], salt)
return salt, verifier
def game_verify(username, password, salt, stored_verifier):
"""Comprueba la contraseña de una cuenta de juego (SRP6 Grunt)."""
if salt is None or stored_verifier is None:
return False
if isinstance(stored_verifier, memoryview):
stored_verifier = bytes(stored_verifier)
calc = game_calculate_verifier(username, password[:16], salt)
return int.from_bytes(calc, "little") == int.from_bytes(stored_verifier, "little")
# ---------------------------------------------------------------------------
# Helpers de acceso a la BD auth (bnet <-> cuentas de juego)
# ---------------------------------------------------------------------------
def normalize_email(email):
"""El email en battlenet_accounts se almacena en MAYÚSCULAS."""
return email.strip().upper()
def get_bnet_account_by_email(cursor, email):
"""Devuelve dict {id, email, srp_version, salt, verifier} o None."""
cursor.execute(
"SELECT id, email, srp_version, salt, verifier "
"FROM battlenet_accounts WHERE email = %s",
[normalize_email(email)],
)
row = cursor.fetchone()
if not row:
return None
return {
"id": row[0],
"email": row[1],
"srp_version": row[2],
"salt": row[3],
"verifier": row[4],
}
def get_game_accounts_for_bnet(cursor, bnet_id):
"""Lista de cuentas de juego de una cuenta bnet: [{id, username, index}]."""
cursor.execute(
"SELECT id, username, battlenet_index "
"FROM account WHERE battlenet_account = %s ORDER BY battlenet_index",
[bnet_id],
)
return [{"id": r[0], "username": r[1], "index": r[2]} for r in cursor.fetchall()]
def get_next_battlenet_index(cursor, bnet_id):
"""Siguiente battlenet_index libre para una cuenta bnet (1-based)."""
cursor.execute(
"SELECT MAX(battlenet_index) FROM account WHERE battlenet_account = %s",
[bnet_id],
)
row = cursor.fetchone()
return (row[0] or 0) + 1
def make_game_account_username(bnet_id, index=1):
"""Nombre de la cuenta de juego: ``<bnetId>#<index>`` (como TrinityCore)."""
return f"{bnet_id}#{index}"
def get_battlepay_credits(cursor, bnet_id):
"""Créditos Battlepay de la cuenta bnet (0 si la columna no existe)."""
if not bnet_id:
return 0
try:
cursor.execute("SELECT battlePayCredits FROM battlenet_accounts WHERE id = %s", [bnet_id])
row = cursor.fetchone()
return row[0] if row and row[0] is not None else 0
except Exception:
return 0
-40
View File
@@ -1,40 +0,0 @@
from django.conf import settings
from .models import ServerSelection
from django.db import connections
import socket
def url_principal(request):
"""Devuelve la URL principal del sitio."""
current_domain = f"{request.scheme}://{request.get_host()}"
return {'URL_PRINCIPAL': current_domain}
def configuracion_global(request):
"""Devuelve configuraciones globales como el nombre del servidor y el año actual."""
return {
'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR,
'ANIO_ACTUAL': settings.ANIO_ACTUAL,
'TURNSTILE_SITE_KEY': settings.TURNSTILE_SITE_KEY,
}
def get_server_info(request):
"""Devuelve información del servidor para que esté disponible en todas las plantillas."""
server_selection = ServerSelection.objects.first()
online_count = 0
if server_selection:
# Obtener la cantidad de personajes en línea
try:
with connections['acore_characters'].cursor() as cursor:
cursor.execute("SELECT COUNT(*) FROM characters WHERE online = 1")
result = cursor.fetchone()
if result is not None:
online_count = result[0]
except Exception as e:
# Manejar errores de conexión o consulta
print(f"Error al obtener personajes en línea: {e}")
return {
'server': server_selection,
'online_characters': online_count,
'expansion': server_selection.expansion if server_selection else 'Unknown',
}
-35
View File
@@ -1,35 +0,0 @@
import os
from django.core.mail import send_mail
from django.conf import settings
from django.template.loader import render_to_string
from django.utils.html import strip_tags
def enviar_correo(subject, to_email, template, context):
"""
Envía un correo electrónico utilizando una plantilla HTML.
Args:
subject (str): El asunto del correo.
to_email (str): Dirección de correo del destinatario.
template (str): Ruta de la plantilla HTML.
context (dict): Contexto para renderizar la plantilla.
"""
try:
# Renderizar el contenido del correo desde una plantilla
html_message = render_to_string(template, context)
plain_message = strip_tags(html_message)
from_email = settings.DEFAULT_FROM_EMAIL
# Enviar el correo
send_mail(
subject,
plain_message,
from_email,
[to_email],
html_message=html_message,
fail_silently=False,
)
return True
except Exception as e:
print(f"Error al enviar el correo: {e}")
return False
-28
View File
@@ -1,28 +0,0 @@
from django.shortcuts import redirect
from django.urls import resolve, reverse
from .models import MaintenanceMode
class MaintenanceMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Excluir todas las rutas bajo /admin y la propia página de mantenimiento
excluded_prefixes = [
reverse('admin:index').rstrip('/'), # Asegura que todas las rutas bajo /admin estén excluidas
reverse('maintenance').rstrip('/'), # Página de mantenimiento
]
# Verificar si la ruta actual está en los prefijos excluidos
if any(request.path.startswith(prefix) for prefix in excluded_prefixes):
return self.get_response(request)
# Verificar si el modo de mantenimiento está activo
try:
maintenance = MaintenanceMode.objects.first()
if maintenance and maintenance.is_active:
return redirect('maintenance')
except MaintenanceMode.DoesNotExist:
pass
return self.get_response(request)
-114
View File
@@ -1,114 +0,0 @@
# Generated by Django 5.1.2 on 2024-11-11 16:11
import django.db.models.deletion
import django_ckeditor_5.fields
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='ClienteCategoria',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nombre', models.CharField(max_length=100)),
('descripcion', models.CharField(blank=True, max_length=200, null=True)),
],
),
migrations.CreateModel(
name='ContentCreator',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('description', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Descripción')),
('content_type', models.CharField(default='Contenido general', max_length=60)),
('subtitle', models.CharField(default='Nova WoW', max_length=60)),
('youtube_video_url', models.URLField(blank=True, max_length=300, null=True)),
('avatar_image_url', models.URLField(blank=True, max_length=300, null=True)),
('youtube_channel_url', models.URLField(blank=True, max_length=300, null=True)),
('facebook_page_url', models.URLField(blank=True, max_length=300, null=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='DownloadClientPage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(default='Descarga del cliente', max_length=255)),
('content', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Contenido')),
('mac_instructions', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True, verbose_name='Instrucciones para macOS')),
('updated_at', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='Noticia',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('titulo', models.CharField(max_length=200)),
('contenido', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Contenido')),
('fecha_publicacion', models.DateTimeField(auto_now=True)),
('fecha_fin_evento', models.DateField(blank=True, null=True)),
('enlace', models.URLField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='RecruitAFriend',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(default='Recluta a un amigo', max_length=255)),
('information', models.CharField(default='Información', max_length=255)),
('content', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Contenido')),
('updated_at', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='RecruitReward',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('required_friends', models.IntegerField()),
('reward_name', models.CharField(max_length=100)),
('item_id', models.IntegerField()),
('item_quantity', models.IntegerField(default=1)),
('item_link', models.URLField()),
('icon_class', models.CharField(help_text="Clase CSS para el icono (ej. 'icontinyl q3')", max_length=50)),
('claimed_by', models.CharField(blank=True, max_length=100, null=True)),
],
),
migrations.CreateModel(
name='ServerSelection',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('server_id', models.IntegerField()),
('name', models.CharField(max_length=100)),
('address', models.CharField(max_length=100)),
('port', models.IntegerField()),
('gamebuild', models.IntegerField()),
],
),
migrations.CreateModel(
name='ClaimedReward',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('account_id', models.IntegerField()),
('character_name', models.CharField(max_length=100)),
('username', models.CharField(max_length=100)),
('claimed_at', models.DateTimeField(auto_now_add=True)),
('recruit_reward', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='claimed_rewards', to='home.recruitreward')),
],
),
migrations.CreateModel(
name='SistemaOperativo',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nombre', models.CharField(max_length=100)),
('url_descarga', models.URLField()),
('categoria', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sistemas_operativos', to='home.clientecategoria')),
],
),
]
@@ -1,37 +0,0 @@
# Generated by Django 5.1.2 on 2024-11-12 12:56
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='AccountActivation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('username', models.CharField(max_length=17)),
('email', models.EmailField(max_length=254)),
('password', models.CharField(max_length=64)),
('salt', models.CharField(max_length=64)),
('verifier', models.CharField(max_length=128)),
('recruiter_id', models.IntegerField(blank=True, null=True)),
('hash', models.CharField(max_length=32, unique=True)),
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
],
),
migrations.RemoveField(
model_name='recruitreward',
name='claimed_by',
),
migrations.AddField(
model_name='claimedreward',
name='ip_address',
field=models.CharField(default='127.0.0.1', max_length=45),
),
]
@@ -1,18 +0,0 @@
# Generated by Django 5.1.2 on 2024-11-12 13:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0002_accountactivation_remove_recruitreward_claimed_by_and_more'),
]
operations = [
migrations.AlterField(
model_name='accountactivation',
name='salt',
field=models.CharField(max_length=128),
),
]
@@ -1,23 +0,0 @@
# Generated by Django 5.1.2 on 2024-11-12 14:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0003_alter_accountactivation_salt'),
]
operations = [
migrations.AlterField(
model_name='accountactivation',
name='salt',
field=models.BinaryField(max_length=32),
),
migrations.AlterField(
model_name='accountactivation',
name='verifier',
field=models.BinaryField(max_length=32),
),
]
-24
View File
@@ -1,24 +0,0 @@
# Generated by Django 5.1.2 on 2024-11-13 10:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0004_alter_accountactivation_salt_and_more'),
]
operations = [
migrations.CreateModel(
name='SecurityToken',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user_id', models.IntegerField()),
('token', models.CharField(max_length=6)),
('created_at', models.DateTimeField(auto_now_add=True)),
('expires_at', models.DateTimeField()),
('ip_address', models.GenericIPAddressField()),
],
),
]
@@ -1,186 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-26 21:07
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0005_securitytoken'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ChangeFactionPrice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('price', models.DecimalField(decimal_places=2, default=10.0, max_digits=6)),
],
),
migrations.CreateModel(
name='ChangeRacePrice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('price', models.DecimalField(decimal_places=2, max_digits=5)),
],
),
migrations.CreateModel(
name='CustomizePrice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('price', models.DecimalField(decimal_places=2, default=1.0, max_digits=5)),
],
),
migrations.CreateModel(
name='GoldPrice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('gold_amount', models.IntegerField()),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
],
),
migrations.CreateModel(
name='GuildRenameSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('cost', models.IntegerField(default=1000, help_text='Costo en Donation Points (DP) para renombrar una hermandad.')),
],
options={
'verbose_name': 'Configuración de Renombrar Hermandad',
'verbose_name_plural': 'Configuraciones de Renombrar Hermandad',
},
),
migrations.CreateModel(
name='HomeApiPoints',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('accountID', models.IntegerField(unique=True)),
('vp', models.IntegerField(default=0)),
('dp', models.IntegerField(default=0)),
],
options={
'db_table': 'home_api_points',
},
),
migrations.CreateModel(
name='LevelUpPrice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('price', models.DecimalField(decimal_places=2, default=10.0, max_digits=10)),
],
),
migrations.CreateModel(
name='RenamePrice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('price', models.DecimalField(decimal_places=2, default=2.0, max_digits=5)),
],
),
migrations.CreateModel(
name='ReviveHistory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('character_name', models.CharField(max_length=50)),
('used_at', models.DateTimeField()),
],
),
migrations.CreateModel(
name='TransferPrice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('price', models.DecimalField(decimal_places=2, max_digits=5)),
],
),
migrations.CreateModel(
name='UnstuckHistory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('character_name', models.CharField(max_length=50)),
('used_at', models.DateTimeField()),
],
),
migrations.CreateModel(
name='VoteSite',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
('url', models.URLField(help_text='URL del sitio de votación')),
('image_url', models.URLField(help_text='URL completa de la imagen', max_length=300)),
('points', models.IntegerField(default=1, help_text='Puntos otorgados por votar')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
),
migrations.RemoveField(
model_name='securitytoken',
name='user_id',
),
migrations.AddField(
model_name='accountactivation',
name='is_new_email_used',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='accountactivation',
name='is_used',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='accountactivation',
name='old_email',
field=models.EmailField(blank=True, max_length=254, null=True),
),
migrations.AddField(
model_name='accountactivation',
name='old_email_hash',
field=models.CharField(blank=True, max_length=32, null=True),
),
migrations.AddField(
model_name='securitytoken',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.CreateModel(
name='StoreCategory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='home.storecategory')),
],
),
migrations.CreateModel(
name='StoreItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('item_id', models.IntegerField()),
('image_url', models.URLField()),
('price', models.DecimalField(decimal_places=2, max_digits=6)),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='home.storecategory')),
],
),
migrations.CreateModel(
name='Cart',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('username', models.CharField(max_length=150)),
('added_at', models.DateTimeField(auto_now_add=True)),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='home.storeitem')),
],
),
migrations.CreateModel(
name='VoteLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('account_id', models.IntegerField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('last_vote_time', models.DateTimeField(blank=True, null=True)),
('processed', models.BooleanField(default=False)),
('processed_at', models.DateTimeField(blank=True, null=True)),
('vote_site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='home.votesite')),
],
),
]
-20
View File
@@ -1,20 +0,0 @@
# Generated by Django 4.2.17 on 2024-12-28 21:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0006_changefactionprice_changeraceprice_customizeprice_and_more'),
]
operations = [
migrations.CreateModel(
name='MaintenanceMode',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_active', models.BooleanField(default=False)),
],
),
]
@@ -1,49 +0,0 @@
# Generated by Django 5.1.4 on 2025-02-15 18:04
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0007_maintenancemode'),
]
operations = [
migrations.CreateModel(
name='Category',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
],
),
migrations.RemoveField(
model_name='storecategory',
name='parent_category',
),
migrations.RemoveField(
model_name='storeitem',
name='category',
),
migrations.CreateModel(
name='Item',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
('item_id', models.IntegerField(unique=True)),
('image_url', models.URLField()),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='home.category')),
],
),
migrations.DeleteModel(
name='Cart',
),
migrations.DeleteModel(
name='StoreCategory',
),
migrations.DeleteModel(
name='StoreItem',
),
]
-30
View File
@@ -1,30 +0,0 @@
# Generated by Django 5.1.4 on 2025-02-16 18:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0008_category_remove_storecategory_parent_category_and_more'),
]
operations = [
migrations.CreateModel(
name='StripeLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('account_id', models.IntegerField()),
('username', models.CharField(max_length=100)),
('email', models.EmailField(max_length=254)),
('acore_ip', models.GenericIPAddressField()),
('stripe_ip', models.GenericIPAddressField(blank=True, null=True)),
('product_name', models.TextField()),
('amount', models.DecimalField(decimal_places=2, max_digits=10)),
('session_id', models.CharField(max_length=255)),
('mode', models.CharField(choices=[('test', 'Test'), ('live', 'Live')], max_length=10)),
('character_name', models.CharField(max_length=100)),
('timestamp', models.DateTimeField(auto_now_add=True)),
],
),
]
-23
View File
@@ -1,23 +0,0 @@
# Generated by Django 5.1.4 on 2025-02-18 10:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0009_stripelog'),
]
operations = [
migrations.CreateModel(
name='LoginAttempt',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('username', models.CharField(max_length=100)),
('status', models.CharField(max_length=50)),
('ip_address', models.GenericIPAddressField()),
('timestamp', models.DateTimeField(auto_now_add=True)),
],
),
]
@@ -1,32 +0,0 @@
# Generated by Django 5.1.4 on 2025-03-02 16:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0010_loginattempt'),
]
operations = [
migrations.CreateModel(
name='Pedido',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('transaction_id', models.CharField(blank=True, max_length=100, null=True, unique=True)),
('email_cliente', models.EmailField(max_length=254)),
('estado', models.CharField(default='Pendiente', max_length=20)),
('monto', models.DecimalField(decimal_places=2, max_digits=10)),
],
),
migrations.AlterModelOptions(
name='guildrenamesettings',
options={},
),
migrations.AlterField(
model_name='guildrenamesettings',
name='cost',
field=models.DecimalField(decimal_places=2, default=10, help_text='Costo en euros para renombrar una hermandad.', max_digits=10),
),
]
@@ -1,28 +0,0 @@
# Generated by Django 6.0.7 on 2026-07-12 16:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0011_pedido_alter_guildrenamesettings_options_and_more'),
]
operations = [
migrations.AlterField(
model_name='accountactivation',
name='salt',
field=models.BinaryField(blank=True, max_length=32, null=True),
),
migrations.AlterField(
model_name='accountactivation',
name='username',
field=models.CharField(blank=True, max_length=17, null=True),
),
migrations.AlterField(
model_name='accountactivation',
name='verifier',
field=models.BinaryField(blank=True, max_length=256, null=True),
),
]
@@ -1,18 +0,0 @@
# Generated by Django 6.0.7 on 2026-07-12 17:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0012_alter_accountactivation_salt_and_more'),
]
operations = [
migrations.AddField(
model_name='stripelog',
name='fulfilled',
field=models.BooleanField(default=False),
),
]
-24
View File
@@ -1,24 +0,0 @@
# Generated by Django 6.0.7 on 2026-07-12 20:53
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0013_stripelog_fulfilled'),
]
operations = [
migrations.CreateModel(
name='PasswordReset',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(max_length=254)),
('token', models.CharField(max_length=64, unique=True)),
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
('used', models.BooleanField(default=False)),
],
),
]
View File
-13
View File
@@ -1,13 +0,0 @@
"""Modelos de la app `home`, divididos por secciones.
Antes un único home/models.py. El `import *` de cada submódulo re-exporta
los modelos para que `from home.models import X`, el admin y las migraciones
(app_label=`home`, nombres de modelo intactos) sigan funcionando igual.
"""
from .pricing import * # noqa: F401,F403
from .store import * # noqa: F401,F403
from .site import * # noqa: F401,F403
from .recruit import * # noqa: F401,F403
from .accounts import * # noqa: F401,F403
from .voting import * # noqa: F401,F403
from .history import * # noqa: F401,F403
-62
View File
@@ -1,62 +0,0 @@
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
class AccountActivation(models.Model):
# En 3.4.3 (Battle.net) la identidad es el email; el username de la cuenta
# de juego se genera al activar (<bnetId>#1), por eso es opcional aquí.
username = models.CharField(max_length=17, null=True, blank=True)
email = models.EmailField()
old_email = models.EmailField(null=True, blank=True)
password = models.CharField(max_length=64)
# salt/verifier ya no se precalculan en el registro (se calculan al activar,
# tanto el de bnet como el de la cuenta de juego). Se conservan opcionales
# por compatibilidad con el flujo de cambio de email.
salt = models.BinaryField(max_length=32, null=True, blank=True)
verifier = models.BinaryField(max_length=256, null=True, blank=True)
recruiter_id = models.IntegerField(null=True, blank=True)
hash = models.CharField(max_length=32, unique=True)
old_email_hash = models.CharField(max_length=32, null=True, blank=True)
is_used = models.BooleanField(default=False) # Indica si el old_email_hash ha sido usado
is_new_email_used = models.BooleanField(default=False) # Indica si el hash del nuevo correo ha sido usado
created_at = models.DateTimeField(default=timezone.now)
def is_expired(self):
return timezone.now() > self.created_at + timezone.timedelta(hours=1)
def __str__(self):
return f"Activation for {self.username}"
class PasswordReset(models.Model):
"""Token de restablecimiento de contraseña (cuenta Battle.net, por email)."""
email = models.EmailField()
token = models.CharField(max_length=64, unique=True)
created_at = models.DateTimeField(default=timezone.now)
used = models.BooleanField(default=False)
def is_expired(self):
return timezone.now() > self.created_at + timezone.timedelta(hours=1)
def __str__(self):
return f"PasswordReset for {self.email}"
class SecurityToken(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
token = models.CharField(max_length=6)
created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField()
ip_address = models.GenericIPAddressField()
def __str__(self):
return f"Token for {self.user.username if self.user else 'Desconocido'}"
class LoginAttempt(models.Model):
username = models.CharField(max_length=100)
status = models.CharField(max_length=50) # Ejemplo: 'Éxito' o 'Fracaso'
ip_address = models.GenericIPAddressField() # IP del intento de inicio de sesión
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.username} - {self.status} - {self.timestamp}"
-13
View File
@@ -1,13 +0,0 @@
from django.db import models
class UnstuckHistory(models.Model):
character_name = models.CharField(max_length=50)
used_at = models.DateTimeField()
class ReviveHistory(models.Model):
character_name = models.CharField(max_length=50)
used_at = models.DateTimeField()
def __str__(self):
return f"{self.character_name} - {self.used_at}"
-51
View File
@@ -1,51 +0,0 @@
from django.db import models
class ChangeFactionPrice(models.Model):
price = models.DecimalField(max_digits=6, decimal_places=2, default=10.00)
def __str__(self):
return f"Precio para cambiar facción: {self.price} EUR"
class LevelUpPrice(models.Model):
price = models.DecimalField(max_digits=10, decimal_places=2, default=10.00)
def __str__(self):
return f"Subir a nivel 80: {self.price} EUR"
class GoldPrice(models.Model):
gold_amount = models.IntegerField() # Cantidad de oro
price = models.DecimalField(max_digits=10, decimal_places=2) # Precio en EUR
def __str__(self):
return f"{self.gold_amount} oro - {self.price} EUR"
class TransferPrice(models.Model):
price = models.DecimalField(max_digits=5, decimal_places=2)
def __str__(self):
return f"Precio de transferencia: {self.price} EUR"
class RenamePrice(models.Model):
price = models.DecimalField(max_digits=5, decimal_places=2, default=2.00)
def __str__(self):
return f"{self.price} EUR"
class CustomizePrice(models.Model):
price = models.DecimalField(max_digits=5, decimal_places=2, default=1.00)
def __str__(self):
return f"{self.price} EUR"
class ChangeRacePrice(models.Model):
price = models.DecimalField(max_digits=5, decimal_places=2)
def __str__(self):
return f"Cambiar raza: {self.price} EUR"
class GuildRenameSettings(models.Model):
cost = models.DecimalField(max_digits=10, decimal_places=2, default=10, help_text="Costo en euros para renombrar una hermandad.")
def __str__(self):
return f"Configuración de renombrar hermandad - Costo: {self.cost}"
-34
View File
@@ -1,34 +0,0 @@
from django.db import models
from django_ckeditor_5.fields import CKEditor5Field
class RecruitAFriend(models.Model):
title = models.CharField(max_length=255, default="Recluta a un amigo")
information = models.CharField(max_length=255, default="Información")
content = CKEditor5Field('Contenido', config_name='default')
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
class RecruitReward(models.Model):
required_friends = models.IntegerField()
reward_name = models.CharField(max_length=100)
item_id = models.IntegerField()
item_quantity = models.IntegerField(default=1)
item_link = models.URLField()
icon_class = models.CharField(max_length=50, help_text="Clase CSS para el icono (ej. 'icontinyl q3')")
def __str__(self):
return f"{self.required_friends} amigos - {self.reward_name}"
class ClaimedReward(models.Model):
recruit_reward = models.ForeignKey(RecruitReward, on_delete=models.CASCADE, related_name='claimed_rewards')
account_id = models.IntegerField()
character_name = models.CharField(max_length=100)
username = models.CharField(max_length=100)
claimed_at = models.DateTimeField(auto_now_add=True)
ip_address = models.CharField(max_length=45, default='127.0.0.1') # Añadir un valor predeterminado
def __str__(self):
return f"{self.username} - {self.recruit_reward.reward_name} - {self.claimed_at}"
-95
View File
@@ -1,95 +0,0 @@
from django.db import models
from django_ckeditor_5.fields import CKEditor5Field
def expansion_from_gamebuild(gamebuild):
"""Etiqueta de expansión a partir del build del cliente.
Distingue el retail antiguo del relanzamiento WoW Classic:
- 12340 -> 'WotLK' (3.3.5a, retail/privado clásico)
- 44832..54261 -> 'WotLK Classic' (3.4.x; 3.4.3 = 54261)
- 15595 -> 'Cataclysm' (4.3.4 retail)
- 54332..60000 -> 'Cataclysm Classic' (4.4.x)
WotLK Classic llega a 3.4.3=54261 y Cataclysm Classic empieza en 4.4.0=54332,
están muy pegados: de ahí el corte en 54261. Nova WoW corre 3.4.3 -> WotLK Classic.
"""
if gamebuild is None:
return 'Unknown'
if gamebuild == 12340:
return 'WotLK'
if 44000 <= gamebuild <= 54261:
return 'WotLK Classic'
if gamebuild == 15595:
return 'Cataclysm'
if 54262 <= gamebuild <= 60000:
return 'Cataclysm Classic'
return 'Unknown'
class Noticia(models.Model):
titulo = models.CharField(max_length=200)
contenido = CKEditor5Field('Contenido', config_name='default')
fecha_publicacion = models.DateTimeField(auto_now=True) # Cambiado a DateTimeField
fecha_fin_evento = models.DateField(null=True, blank=True)
enlace = models.URLField(max_length=200, blank=True, null=True)
def __str__(self):
return self.titulo
class MaintenanceMode(models.Model):
is_active = models.BooleanField(default=False)
def __str__(self):
return "Maintenance Mode"
class ClienteCategoria(models.Model):
nombre = models.CharField(max_length=100)
descripcion = models.CharField(max_length=200, blank=True, null=True)
def __str__(self):
return self.nombre
class SistemaOperativo(models.Model):
categoria = models.ForeignKey(ClienteCategoria, related_name='sistemas_operativos', on_delete=models.CASCADE)
nombre = models.CharField(max_length=100)
url_descarga = models.URLField()
def __str__(self):
return f"{self.nombre} - {self.categoria.nombre}"
class ServerSelection(models.Model):
server_id = models.IntegerField()
name = models.CharField(max_length=100)
address = models.CharField(max_length=100)
port = models.IntegerField()
gamebuild = models.IntegerField()
@property
def expansion(self):
return expansion_from_gamebuild(self.gamebuild)
def __str__(self):
return f"{self.name} ({self.expansion})"
class DownloadClientPage(models.Model):
title = models.CharField(max_length=255, default="Descarga del cliente")
content = CKEditor5Field('Contenido', config_name='default')
mac_instructions = CKEditor5Field('Instrucciones para macOS', blank=True, null=True, config_name='default')
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
class ContentCreator(models.Model):
title = models.CharField(max_length=200)
description = CKEditor5Field('Descripción', config_name='default')
content_type = models.CharField(max_length=60, default="Contenido general")
subtitle = models.CharField(max_length=60, default="Nova WoW")
youtube_video_url = models.URLField(max_length=300, blank=True, null=True)
avatar_image_url = models.URLField(max_length=300, blank=True, null=True)
youtube_channel_url = models.URLField(max_length=300, blank=True, null=True)
facebook_page_url = models.URLField(max_length=300, blank=True, null=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
-44
View File
@@ -1,44 +0,0 @@
from django.db import models
class Category(models.Model):
name = models.CharField(max_length=100, unique=True)
def __str__(self):
return self.name
class Item(models.Model):
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="items")
name = models.CharField(max_length=200)
price = models.DecimalField(max_digits=10, decimal_places=2) # Precio en euros
item_id = models.IntegerField(unique=True) # ID del ítem en el juego
image_url = models.URLField() # URL de la imagen del ítem
def __str__(self):
return self.name
class StripeLog(models.Model):
account_id = models.IntegerField() # ID de la cuenta en acore_auth
username = models.CharField(max_length=100) # Nombre de la cuenta
email = models.EmailField() # Correo electrónico
acore_ip = models.GenericIPAddressField() # IP de acore_auth
stripe_ip = models.GenericIPAddressField(blank=True, null=True) # IP detectada por Stripe
product_name = models.TextField() # Producto comprado
amount = models.DecimalField(max_digits=10, decimal_places=2) # Precio total
session_id = models.CharField(max_length=255) # ID de la sesión de Stripe
mode = models.CharField(max_length=10, choices=[("test", "Test"), ("live", "Live")]) # Modo de Stripe
character_name = models.CharField(max_length=100) # Nombre del personaje que compró
timestamp = models.DateTimeField(auto_now_add=True) # Fecha y hora
fulfilled = models.BooleanField(default=False) # Si la entrega ya se procesó (evita re-entregas)
def __str__(self):
return f"{self.username} compró {self.product_name} por {self.amount}€ ({self.mode})"
class Pedido(models.Model):
transaction_id = models.CharField(max_length=100, unique=True, null=True, blank=True)
email_cliente = models.EmailField()
estado = models.CharField(max_length=20, default="Pendiente")
monto = models.DecimalField(max_digits=10, decimal_places=2)
def __str__(self):
return f"Pedido {self.transaction_id} - {self.estado}"
-35
View File
@@ -1,35 +0,0 @@
from django.db import models
class VoteSite(models.Model):
name = models.CharField(max_length=100, unique=True)
url = models.URLField(max_length=200, help_text="URL del sitio de votación")
image_url = models.URLField(max_length=300, help_text="URL completa de la imagen")
points = models.IntegerField(default=1, help_text="Puntos otorgados por votar")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
class VoteLog(models.Model):
account_id = models.IntegerField()
vote_site = models.ForeignKey('VoteSite', on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
last_vote_time = models.DateTimeField(null=True, blank=True)
processed = models.BooleanField(default=False)
processed_at = models.DateTimeField(null=True, blank=True)
def __str__(self):
return f"Voto para {self.vote_site.name} por cuenta {self.account_id}"
class HomeApiPoints(models.Model):
accountID = models.IntegerField(unique=True)
vp = models.IntegerField(default=0)
dp = models.IntegerField(default=0)
class Meta:
db_table = 'home_api_points'
def __str__(self):
return f"Cuenta {self.accountID} - PV: {self.vp}, DP: {self.dp}"
-63
View File
@@ -1,63 +0,0 @@
import stripe
from django.conf import settings
from .models import StripeLog
from django.utils.timezone import now
stripe.api_key = settings.STRIPE_SECRET_KEY
def is_session_paid(session_id):
"""True si la sesión de Stripe existe y está realmente pagada."""
if not session_id:
return False
try:
s = stripe.checkout.Session.retrieve(session_id)
return bool(s and getattr(s, "payment_status", None) == "paid")
except Exception:
return False
def create_checkout_session(account_id, username, email, acore_ip, amount, character_name, currency="eur", success_url=None, cancel_url=None, product_name=""):
try:
# Añadir el id de sesión a la success_url para poder verificar el pago
# al volver (Stripe sustituye {CHECKOUT_SESSION_ID} por el id real).
if success_url:
sep = "&" if "?" in success_url else "?"
success_url = f"{success_url}{sep}session_id={{CHECKOUT_SESSION_ID}}"
# 🔹 Crear sesión de pago en Stripe
session = stripe.checkout.Session.create(
payment_method_types=["card"],
line_items=[{
"price_data": {
"currency": currency,
"product_data": {"name": product_name},
"unit_amount": int(amount * 100),
},
"quantity": 1,
}],
mode="payment",
success_url=success_url,
cancel_url=cancel_url,
)
# 🔹 Determinar si Stripe está en modo TEST o LIVE
mode = "live" if settings.STRIPE_SECRET_KEY.startswith("sk_live") else "test"
# 🔹 Guardar el log en la base de datos
StripeLog.objects.create(
account_id=account_id,
username=username,
email=email,
acore_ip=acore_ip,
stripe_ip=None, # Se actualizará cuando llegue el webhook
product_name=product_name,
amount=amount,
session_id=session.id,
mode=mode,
character_name=character_name,
timestamp=now()
)
return {"success": True, "session_id": session.id}
except Exception as e:
return {"success": False, "error": str(e)}
-72
View File
@@ -1,72 +0,0 @@
from django.conf import settings
import requests
SUMUP_CLIENT_ID = settings.SUMUP_CLIENT_ID
SUMUP_CLIENT_SECRET = settings.SUMUP_CLIENT_SECRET
SUMUP_EMAIL = settings.SUMUP_MERCHANT_EMAIL
API_BASE = "https://api.sumup.com"
def obtener_token():
"""Obtiene un access token de SumUp (client_credentials)."""
url = f"{API_BASE}/token"
data = {
"grant_type": "client_credentials",
"client_id": SUMUP_CLIENT_ID,
"client_secret": SUMUP_CLIENT_SECRET,
}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
response = requests.post(url, headers=headers, data=data, timeout=15)
if response.status_code == 200:
return response.json().get("access_token")
raise Exception(f"Error al obtener token: {response.text}")
def crear_checkout_battlepay(reference, monto, descripcion, moneda="EUR", return_url=None):
"""
Crea un checkout de SumUp para una orden Battlepay concreta.
``reference`` es la referencia única de la orden (battlepay_orders.reference);
se usa como ``checkout_reference`` para poder mapear el webhook a la orden.
Devuelve el JSON del checkout (incluye ``id``).
"""
access_token = obtener_token()
url = f"{API_BASE}/v0.1/checkouts"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
data = {
"checkout_reference": reference,
"amount": float(monto),
"currency": moneda,
"pay_to_email": SUMUP_EMAIL,
"description": descripcion,
}
if return_url:
data["return_url"] = return_url
response = requests.post(url, headers=headers, json=data, timeout=15)
if response.status_code == 201:
return response.json()
raise Exception(f"Error al crear checkout: {response.text}")
def obtener_checkout(checkout_id):
"""Consulta un checkout por id. Devuelve su JSON (status, checkout_reference...)."""
access_token = obtener_token()
url = f"{API_BASE}/v0.1/checkouts/{checkout_id}"
headers = {"Authorization": f"Bearer {access_token}"}
response = requests.get(url, headers=headers, timeout=15)
if response.status_code == 200:
return response.json()
raise Exception(f"Error al consultar checkout: {response.text}")
def crear_checkout(monto, moneda="EUR"):
"""Compatibilidad: checkout genérico de prueba. Devuelve el checkout_id."""
checkout = crear_checkout_battlepay("pedido-prueba", monto, "Pago de prueba", moneda)
return checkout.get("id")
-9
View File
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/not_found.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-9
View File
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/ban_history.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-60
View File
@@ -1,60 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
<div class="main-page">
<div class="middle-content">
<div class="body-content">
<div class="title-content">
<h1>Tienda del cliente (Battlepay)</h1>
</div>
<div class="box-content">
<div class="body-box-content centered">
<p>Compras pendientes de pago de la cuenta <strong>{{ username }}</strong>.</p>
<br>
{% if orders %}
<table class="middle-center-table">
<thead>
<tr>
<th>Producto</th>
<th>Precio</th>
<th></th>
</tr>
</thead>
<tbody>
{% for order in orders %}
<tr>
<td>{{ order.product_name }}</td>
<td>{{ order.price_eur }} €</td>
<td>
<form action="{% url 'battlepay_pay' %}" method="POST" accept-charset="utf-8">
{% csrf_token %}
<input type="hidden" name="reference" value="{{ order.reference }}">
<button type="submit" class="login-button">Pagar con SumUp</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No tienes compras pendientes.</p>
<br>
<p>Para comprar, pulsa <strong>Comprar</strong> en la tienda del cliente dentro del juego;
la compra aparecerá aquí para completar el pago.</p>
{% endif %}
<hr>
<p><a href="my-account">Volver a mi cuenta</a></p>
</div>
</div>
</div>
</div>
</div>
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-9
View File
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/change_email.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/change_faction.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/change_faction_cancel.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/change_faction_success.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/change_password.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-9
View File
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/change_race.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/change_race_success.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/change_race_success.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/confirm_new_email_success.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/confirm_old_email_success.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/customize_character.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/customize_success.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-9
View File
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/d_points.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-9
View File
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/expired_link.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-9
View File
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/gold_cancel.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/gold_character.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-9
View File
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/gold_success.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/guild_rename_cancel.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/guild_rename_success.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-9
View File
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/level_up.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/level_up_cancel.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/level_up_success.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-9
View File
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/pago_sumup.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/points_history.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-9
View File
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/promo_code.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/quest_character.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/rename_cancel.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/rename_character.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-9
View File
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/rename_guild.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/rename_success.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/restore_character.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/restore_items.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/revive_character.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/security_history.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/security_token.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-9
View File
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/send_gift.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-9
View File
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/trade_points.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/trans_history.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/transfer_cancel.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/transfer_character.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/transfer_d_points.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/transfer_success.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/unstuck_character.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-9
View File
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/vote_points.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,44 +0,0 @@
{% extends "admin/change_list.html" %}
{% load i18n %}
{% block content %}
<div class="container mt-4">
<h1 class="mb-4">{% trans "Seleccionar Servidor" %}</h1>
<form method="post" action="{% url 'admin:home_serverselection_changelist' %}" class="needs-validation" novalidate>
{% csrf_token %}
<div class="form-group">
<label for="serverSelection">{% trans "Seleccionar servidor" %}</label>
<select class="form-control" id="serverSelection" name="server_selection" required>
<option value="" disabled selected>{% trans "Elija un servidor" %}</option>
{% for server in servers %}
<option value="{{ server.0 }}">{{ server.1 }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
{% trans "Debe seleccionar un servidor antes de continuar." %}
</div>
</div>
<div class="submit-row">
<input type="submit" value='{% trans "Guardar selección" %}' class="default" name="_save">
</div>
</form>
</div>
<script>
// Validación de formulario con Bootstrap
(function () {
'use strict';
window.addEventListener('load', function () {
var forms = document.getElementsByClassName('needs-validation');
Array.prototype.filter.call(forms, function (form) {
form.addEventListener('submit', function (event) {
if (form.checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
}, false);
});
}, false);
})();
</script>
{% endblock %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/activation_invalid.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/activation_success.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-9
View File
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/login.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-9
View File
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/recover_account.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-9
View File
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/register.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-29
View File
@@ -1,29 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% load django_vite %}
<div class="main-page">
<div class="middle-content">
<div class="body-content">
<div class="title-content">
<h1>Restablecer contraseña</h1>
</div>
<div class="box-content">
<!-- Isla React (Vite): fija la nueva contraseña con el token del enlace.
POST a {% url 'reset_password' %} (JSON) -> re-deriva el verifier bnet. -->
<div id="reset-password-app"
data-csrf="{{ csrf_token }}"
data-valid="{{ valid|yesno:'true,false' }}"
data-token="{{ token }}"
data-reset-url="{% url 'reset_password' %}"
data-recover-url="{% url 'recover' %}"></div>
{% vite_asset 'src/entries/reset_password.tsx' %}
</div>
</div>
</div>
</div>
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-45
View File
@@ -1,45 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
<div class="main-page">
<div class="middle-content">
<div class="body-content">
<div class="title-content">
<h1>Selecciona tu cuenta de juego</h1>
</div>
<div class="box-content">
<div class="body-box-content centered">
{% if bnet_email %}
<p>Conectado como <strong>{{ bnet_email }}</strong></p>
<br>
{% endif %}
{% if game_accounts %}
<p>Tu cuenta Battle.net tiene varias cuentas de juego. Elige con cuál quieres continuar:</p>
<br>
{% load django_vite %}
<!-- Isla React (Vite): selector de cuenta de juego. Selección vía GET
a /api/account/select/ (sin POST); redirige a my-account. -->
<div id="select-account-app" data-select-url="/api/account/select/"></div>
{{ game_accounts|json_script:"select-account-data" }}
{% vite_asset 'src/entries/select_account.tsx' %}
{% else %}
<p>No se han encontrado cuentas de juego asociadas a esta cuenta Battle.net.</p>
<br>
<p>Contacta con el soporte si crees que es un error.</p>
{% endif %}
<hr>
<p><a href="log-out">Cerrar sesión</a></p>
</div>
</div>
</div>
</div>
</div>
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-10
View File
@@ -1,10 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/start_time.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/noticias.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/videos.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-9
View File
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/help.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-9
View File
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/contact_us.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
@@ -1,9 +0,0 @@
<!DOCTYPE html>
{% include 'partials/head.html' %}
{% include 'partials/header.html' %}
{% include 'partials/video.html' %}
{% include 'partials/descargas.html' %}
{% include 'partials/social.html' %}
{% include 'partials/footer.html' %}
{% include 'partials/final.html' %}
-34
View File
@@ -1,34 +0,0 @@
<!DOCTYPE html>
<html lang="es">
<head><meta charset="utf-8"></head>
<body style="margin:0;padding:0;background:#1b120b;font-family:Arial,Helvetica,sans-serif;color:#e8dccb;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#1b120b;padding:24px 0;">
<tr>
<td align="center">
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:#241812;border:1px solid #4a3320;border-radius:8px;">
<tr>
<td style="padding:28px 32px;">
<h1 style="margin:0 0 8px;font-size:20px;color:#d79602;">{{ NOMBRE_SERVIDOR }}</h1>
<h2 style="margin:0 0 16px;font-size:16px;color:#e8dccb;">Tus cuentas de juego</h2>
<p style="margin:0 0 12px;line-height:1.5;">Hola,</p>
<p style="margin:0 0 16px;line-height:1.5;">Estas son las cuentas de juego asociadas a tu cuenta Battle.net (<strong>{{ email }}</strong>). Recuerda: para iniciar sesión en la web usas tu <strong>correo electrónico</strong>.</p>
<table role="presentation" cellpadding="0" cellspacing="0" style="width:100%;border-collapse:collapse;margin:0 0 16px;">
{% for a in accounts %}
<tr>
<td style="padding:10px 14px;border:1px solid #4a3320;background:#2c1e14;color:#e8dccb;">{{ a.username }}</td>
</tr>
{% empty %}
<tr>
<td style="padding:10px 14px;border:1px solid #4a3320;background:#2c1e14;color:#b1997f;">No hay cuentas de juego asociadas todavía.</td>
</tr>
{% endfor %}
</table>
<p style="margin:0;line-height:1.5;font-size:13px;color:#b1997f;">Si no has solicitado esta información, ignora este correo.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
-145
View File
@@ -1,145 +0,0 @@
<td valign="top" style="padding:0;Margin:0">
<table cellpadding="0" cellspacing="0" align="center" style="border-collapse:collapse;border-spacing:0px;table-layout:fixed!important;width:100%;background-color:transparent;background-repeat:repeat;background-position:center top">
<tbody><tr style="border-collapse:collapse">
<td align="center" style="padding:0;Margin:0">
<table cellspacing="0" cellpadding="0" align="center" style="border-collapse:collapse;border-spacing:0px;background-color:transparent;width:600px">
<tbody><tr style="border-collapse:collapse">
<td align="left" background="https://ci3.googleusercontent.com/meips/ADKq_Nb7TRH9h2_l1rekf1lKsc5H3P70u9ihoUZEEdHemt7tmVs_0-laBoolKLbqcFXKopQB6mbAu6STs5rmDUPDPSNJr-ZQn1Lg7qTMNpXBfdb-tjYQ9aa9wLiv87TZh1-7Twqjyh_K=s0-d-e1-ft#{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-mails/nw-mail-footer.jpg" style="Margin:0;padding-top:10px;padding-bottom:10px;padding-left:10px;padding-right:10px;background-image:url(https://ci3.googleusercontent.com/meips/ADKq_Nb7TRH9h2_l1rekf1lKsc5H3P70u9ihoUZEEdHemt7tmVs_0-laBoolKLbqcFXKopQB6mbAu6STs5rmDUPDPSNJr-ZQn1Lg7qTMNpXBfdb-tjYQ9aa9wLiv87TZh1-7Twqjyh_K=s0-d-e1-ft#{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-mails/nw-mail-footer.jpg);background-repeat:no-repeat;background-position:left top">
<table cellspacing="0" cellpadding="0" align="left" style="border-collapse:collapse;border-spacing:0px;float:left">
<tbody><tr style="border-collapse:collapse">
<td align="left" style="padding:0;Margin:0;width:280px">
<table width="100%" cellspacing="0" cellpadding="0" role="presentation" style="border-collapse:collapse;border-spacing:0px">
<tbody><tr style="border-collapse:collapse">
<td align="left" id="m_8140815850608678152title-mail-header" style="padding:0;Margin:0"><p style="Margin:0;font-size:11px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';line-height:17px;color:#b1997f">ACTIVACIÓN DE CUENTA</p></td>
</tr>
</tbody></table></td>
</tr>
</tbody></table>
<table cellspacing="0" cellpadding="0" align="right" style="border-collapse:collapse;border-spacing:0px;float:right">
<tbody><tr style="border-collapse:collapse">
<td align="left" style="padding:0;Margin:0">
<table width="100%" cellspacing="0" cellpadding="0" role="presentation" style="border-collapse:collapse;border-spacing:0px">
<tbody><tr style="border-collapse:collapse">
<td style="padding:0;Margin:0">
<table width="100%" cellspacing="0" cellpadding="0" role="presentation" style="border-collapse:collapse;border-spacing:0px">
<tbody><tr id="m_8140815850608678152header-mail-links" style="border-collapse:collapse">
<td style="Margin:0;padding-left:5px;padding-right:5px;padding-top:3px;padding-bottom:0px;border:0" width="50%" bgcolor="transparent" align="center"><a style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';font-size:14px;text-decoration:none;display:block;color:#2471a9" href="https://079xk.mjt.lu/lnk/AU4AAFT1m8YAAAAAAAAAAlmz1CcAAYCtSIUAAAAAACPUeABnKdnNbMFetHGyQ9ursJAD3kGn5wAhkNU/1/YPPqGZJ6WcWgL8kfJbV5GQ/aHR0cHM6Ly91bHRpbW93b3cuY29tL2VzLw" target="_blank" data-saferedirecturl="https://www.google.com/url?q=https://079xk.mjt.lu/lnk/AU4AAFT1m8YAAAAAAAAAAlmz1CcAAYCtSIUAAAAAACPUeABnKdnNbMFetHGyQ9ursJAD3kGn5wAhkNU/1/YPPqGZJ6WcWgL8kfJbV5GQ/aHR0cHM6Ly91bHRpbW93b3cuY29tL2VzLw&amp;source=gmail&amp;ust=1731498433468000&amp;usg=AOvVaw1XHA5FQ8SJ6azBxrP1QOUB">Ir al sitio</a></td>
</tr>
</tbody></table></td>
</tr>
</tbody></table></td>
</tr>
</tbody></table>
</td>
</tr>
</tbody></table></td>
</tr>
</tbody></table>
<table cellspacing="0" cellpadding="0" align="center" style="border-collapse:collapse;border-spacing:0px;table-layout:fixed!important;width:100%">
<tbody><tr style="border-collapse:collapse">
<td align="center" style="padding:0;Margin:0">
<table style="border-collapse:collapse;border-spacing:0px;background-color:transparent;width:600px" cellspacing="0" cellpadding="0" align="center">
<tbody><tr style="border-collapse:collapse">
<td align="left" style="padding:0;Margin:0">
<table cellpadding="0" cellspacing="0" width="100%" style="border-collapse:collapse;border-spacing:0px">
<tbody><tr style="border-collapse:collapse">
<td align="center" valign="top" style="padding:0;Margin:0;width:600px">
<table cellpadding="0" cellspacing="0" width="100%" role="presentation" style="border-collapse:collapse;border-spacing:0px">
<tbody><tr style="border-collapse:collapse">
<td align="center" style="padding:0;Margin:0;font-size:0px"><a href="https://079xk.mjt.lu/lnk/AU4AAFT1m8YAAAAAAAAAAlmz1CcAAYCtSIUAAAAAACPUeABnKdnNbMFetHGyQ9ursJAD3kGn5wAhkNU/2/LcFesQN9Atu1p8XKYNeYrw/aHR0cHM6Ly91bHRpbW93b3cuY29tL2VzLw" style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';font-size:14px;text-decoration:underline;color:#b58a60" target="_blank" data-saferedirecturl="https://www.google.com/url?q=https://079xk.mjt.lu/lnk/AU4AAFT1m8YAAAAAAAAAAlmz1CcAAYCtSIUAAAAAACPUeABnKdnNbMFetHGyQ9ursJAD3kGn5wAhkNU/2/LcFesQN9Atu1p8XKYNeYrw/aHR0cHM6Ly91bHRpbW93b3cuY29tL2VzLw&amp;source=gmail&amp;ust=1731498433468000&amp;usg=AOvVaw3nPd4Lj8GE_62E_stVyXcu"><img src="https://ci3.googleusercontent.com/meips/ADKq_NbGrtaFRw1N5A4Ocfm1t4FL2cRnrcqdXNd7NS7wmpPr_a95bSWKHdPsxbJ7EELaQeomHnmCGgOTiKRXinebDgkWcljQLlRqdekcgfDYtBHevrlgbYUerJF7Kd8q96eogRMAUA=s0-d-e1-ft#{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-mails/nw-mail-logo.webp" alt="" style="display:block;border:0;outline:none;text-decoration:none" width="600" class="CToWUd" data-bit="iit"></a></td>
</tr>
</tbody></table></td>
</tr>
</tbody></table></td>
</tr>
<tr style="border-collapse:collapse">
<td align="left" background="{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-mails/nw-mail-body.jpg#{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-mails/nw-mail-body.jpg" style="Margin:0;padding-top:40px;padding-bottom:40px;padding-left:40px;padding-right:40px;background-image:url({{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-mails/nw-mail-body.jpg#{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-mails/nw-mail-body.jpg);background-repeat:repeat;background-position:left top">
<table width="100%" cellspacing="0" cellpadding="0" style="border-collapse:collapse;border-spacing:0px">
<tbody><tr style="border-collapse:collapse">
<td valign="top" align="center" style="padding:0;Margin:0;width:520px">
<table width="100%" cellspacing="0" cellpadding="0" role="presentation" style="border-collapse:collapse;border-spacing:0px">
<tbody><tr style="border-collapse:collapse">
<td align="left" style="padding:0;Margin:0"><h1 style="Margin:0;line-height:45px;font-family:georgia,times,'times new roman',serif;font-size:30px;font-style:normal;font-weight:normal;color:#b17c02">Bienvenido, {{ username }}</h1></td>
</tr>
<tr style="border-collapse:collapse">
<td align="left" style="padding:0;Margin:0"><p style="Margin:0;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';line-height:21px;color:#ebdec2">Gracias por crear una cuenta en <span class="il">{{ NOMBRE_SERVIDOR }}</span>.<br>Para poder empezar a usar tu cuenta, deberás activarla haciendo clic en el botón ACTIVAR LA CUENTA.<br><br>Tus datos de cuenta son:</p></td>
</tr>
<tr style="border-collapse:collapse">
<td align="left" style="padding:0;Margin:0"><p style="Margin:0;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';line-height:21px;color:#b1997f">Cuenta: {{ username }}<br>Contraseña: {{ password }}<br><br></p></td>
</tr>
<tr style="border-collapse:collapse">
<td align="left" style="padding:0;Margin:0"><p style="Margin:0;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';line-height:21px;color:#ebdec2">Con tu usuario y contraseña podrás entrar a nuestro sitio web y al servidor.</p></td>
</tr>
<tr style="border-collapse:collapse">
<td align="center" style="padding:20px;Margin:0;font-size:0">
<table border="0" width="100%" height="100%" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px">
<tbody><tr style="border-collapse:collapse">
<td style="padding:0;Margin:0;border-bottom:2px solid #5c4c44;background:none;height:1px;width:100%;margin:0px"></td>
</tr>
</tbody></table></td>
</tr>
<tr style="border-collapse:collapse">
<td align="left" style="padding:0;Margin:0;padding-top:5px;padding-bottom:20px"><p style="Margin:0;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';line-height:21px;color:#ebdec2">- Al servidor se debe entrar con nombre de usuario y contraseña. NO con el correo.<br>- Puedes usar el mismo correo para registrar hasta 10 cuentas en el servidor.<br>- El usuario del foro es independiente del usuario que acabas de crear. Puedes crear una cuenta para el foro <a href="https://079xk.mjt.lu/lnk/AU4AAFT1m8YAAAAAAAAAAlmz1CcAAYCtSIUAAAAAACPUeABnKdnNbMFetHGyQ9ursJAD3kGn5wAhkNU/3/oCFCOOvfQvjar6zSGHXF5g/aHR0cHM6Ly9mb3JvLnVsdGltb3dvdy5jb20v" target="_blank" data-saferedirecturl="https://www.google.com/url?q=https://079xk.mjt.lu/lnk/AU4AAFT1m8YAAAAAAAAAAlmz1CcAAYCtSIUAAAAAACPUeABnKdnNbMFetHGyQ9ursJAD3kGn5wAhkNU/3/oCFCOOvfQvjar6zSGHXF5g/aHR0cHM6Ly9mb3JvLnVsdGltb3dvdy5jb20v&amp;source=gmail&amp;ust=1731498433468000&amp;usg=AOvVaw2RiVgc2wlXg1AAzyT2ij7D">aquí</a>.</p></td>
</tr>
<tr style="border-collapse:collapse">
<td align="center" style="padding:0;Margin:0"><span style="border-style:solid;border-color:#211917;background:#211917;border-width:2px;display:inline-block;border-radius:0px;width:auto"><a href="{{ activation_link }}" style="text-decoration:none;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';font-size:16px;color:#ebdec2;border-style:solid;border-color:#211917;border-width:10px 20px 10px 20px;display:inline-block;background:#211917;border-radius:0px;font-weight:normal;font-style:normal;line-height:19px;width:auto;text-align:center" target="_blank" data-saferedirecturl="{{ activation_link }}">ACTIVAR LA CUENTA</a></span></td>
</tr>
<tr style="border-collapse:collapse">
<td align="left" style="padding:0;Margin:0;padding-top:15px"><p style="Margin:0;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';line-height:21px;color:#ebdec2">Si no puedes activar la cuenta usando el botón, copia y pega en tu navegador siguiente enlace:<br><a href="{{ activation_link }}" target="_blank" data-saferedirecturl="{{ activation_link }}">{{ activation_link }}</a></p></td>
</tr>
<tr style="border-collapse:collapse">
<td align="left" style="padding:0;Margin:0;padding-top:5px;padding-bottom:20px"><p style="Margin:0;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';line-height:21px;color:#ebdec2"><br><br>Atentamente, el Equipo de <span class="il">{{ NOMBRE_SERVIDOR }}</span>.</p></td>
</tr>
</tbody></table></td>
</tr>
</tbody></table></td>
</tr>
</tbody></table></td>
</tr>
</tbody></table>
<table cellpadding="0" cellspacing="0" align="center" style="border-collapse:collapse;border-spacing:0px;table-layout:fixed!important;width:100%;background-color:transparent;background-repeat:repeat;background-position:center top">
<tbody><tr style="border-collapse:collapse">
<td align="center" style="padding:0;Margin:0">
<table cellspacing="0" cellpadding="0" align="center" style="background-color:transparent;border-collapse:collapse;border-spacing:0px;width:600px">
<tbody><tr style="border-collapse:collapse">
<td align="left" style="padding:0;Margin:0">
<table width="100%" cellspacing="0" cellpadding="0" style="border-collapse:collapse;border-spacing:0px">
<tbody><tr style="border-collapse:collapse">
<td valign="top" align="center" style="padding:0;Margin:0;width:600px">
<table width="100%" cellspacing="0" cellpadding="0" background="https://ci3.googleusercontent.com/meips/ADKq_Nb7TRH9h2_l1rekf1lKsc5H3P70u9ihoUZEEdHemt7tmVs_0-laBoolKLbqcFXKopQB6mbAu6STs5rmDUPDPSNJr-ZQn1Lg7qTMNpXBfdb-tjYQ9aa9wLiv87TZh1-7Twqjyh_K=s0-d-e1-ft#{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-mails/nw-mail-footer.jpg" style="border-collapse:collapse;border-spacing:0px;background-image:url(https://ci3.googleusercontent.com/meips/ADKq_Nb7TRH9h2_l1rekf1lKsc5H3P70u9ihoUZEEdHemt7tmVs_0-laBoolKLbqcFXKopQB6mbAu6STs5rmDUPDPSNJr-ZQn1Lg7qTMNpXBfdb-tjYQ9aa9wLiv87TZh1-7Twqjyh_K=s0-d-e1-ft#{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-mails/nw-mail-footer.jpg);background-repeat:no-repeat;background-position:left top" role="presentation">
<tbody><tr style="border-collapse:collapse">
<td align="center" style="padding:0;Margin:0;padding-top:10px"><p style="Margin:0;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';line-height:21px;color:#ffffff">SÍGUENOS EN</p></td>
</tr>
<tr style="border-collapse:collapse">
<td align="center" style="padding:0;Margin:0;padding-top:15px;padding-bottom:15px;font-size:0">
<table cellspacing="0" cellpadding="0" role="presentation" style="border-collapse:collapse;border-spacing:0px">
<tbody><tr style="border-collapse:collapse">
<td valign="top" align="center" style="padding:0;Margin:0;padding-right:20px"><a href="https://079xk.mjt.lu/lnk/AU4AAFT1m8YAAAAAAAAAAlmz1CcAAYCtSIUAAAAAACPUeABnKdnNbMFetHGyQ9ursJAD3kGn5wAhkNU/5/f6s0ZwRQuTE-C-jasxOasA/aHR0cHM6Ly93d3cuZmFjZWJvb2suY29tL1VsdGltb1dvVy8" style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';font-size:14px;text-decoration:underline;color:#333333" target="_blank" data-saferedirecturl="https://www.google.com/url?q=https://079xk.mjt.lu/lnk/AU4AAFT1m8YAAAAAAAAAAlmz1CcAAYCtSIUAAAAAACPUeABnKdnNbMFetHGyQ9ursJAD3kGn5wAhkNU/5/f6s0ZwRQuTE-C-jasxOasA/aHR0cHM6Ly93d3cuZmFjZWJvb2suY29tL1VsdGltb1dvVy8&amp;source=gmail&amp;ust=1731498433469000&amp;usg=AOvVaw2ZYRFnIqpWDQyggiWBGXiO"><img title="Facebook" src="{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-mails/facebook-logo-gray.png#{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-mails/facebook-logo-gray.png" alt="Fb" width="32" height="32" style="display:block;border:0;outline:none;text-decoration:none" class="CToWUd" data-bit="iit"></a></td>
<td valign="top" align="center" style="padding:0;Margin:0;padding-right:20px"><a href="https://079xk.mjt.lu/lnk/AU4AAFT1m8YAAAAAAAAAAlmz1CcAAYCtSIUAAAAAACPUeABnKdnNbMFetHGyQ9ursJAD3kGn5wAhkNU/6/UGtcJN5ISi7MAnPkbxjBcQ/aHR0cHM6Ly93d3cuaW5zdGFncmFtLmNvbS9VbHRpbW9Xb1cv" style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';font-size:14px;text-decoration:underline;color:#333333" target="_blank" data-saferedirecturl="https://www.google.com/url?q=https://079xk.mjt.lu/lnk/AU4AAFT1m8YAAAAAAAAAAlmz1CcAAYCtSIUAAAAAACPUeABnKdnNbMFetHGyQ9ursJAD3kGn5wAhkNU/6/UGtcJN5ISi7MAnPkbxjBcQ/aHR0cHM6Ly93d3cuaW5zdGFncmFtLmNvbS9VbHRpbW9Xb1cv&amp;source=gmail&amp;ust=1731498433469000&amp;usg=AOvVaw3a1MAk7G2C1gKYxEBcZ596"><img title="Instagram" src="https://ci3.googleusercontent.com/meips/ADKq_NaAaG1uiPhc-J3UJaRHNGlIjQEP6NvhOLKG9_MBwf-udq3Ao7oCHffCGaQTQRl4AE6vTB11LigeSFs35bZyvLfIeaMY1hq_NtkoC4Zdo-foNc8Tf8PTqEKcvTglkSt8CmOLt4Ewrzuz8jk=s0-d-e1-ft#{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-mails/instagram-logo-gray.png" alt="Ig" width="32" height="32" style="display:block;border:0;outline:none;text-decoration:none" class="CToWUd" data-bit="iit"></a></td>
<td valign="top" align="center" style="padding:0;Margin:0;padding-right:20px"><a href="https://079xk.mjt.lu/lnk/AU4AAFT1m8YAAAAAAAAAAlmz1CcAAYCtSIUAAAAAACPUeABnKdnNbMFetHGyQ9ursJAD3kGn5wAhkNU/7/dNuKqBpwdnPzZqmaJqBMLg/aHR0cHM6Ly90d2l0dGVyLmNvbS9VbHRpbW9Xb1c" style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';font-size:14px;text-decoration:underline;color:#333333" target="_blank" data-saferedirecturl="https://www.google.com/url?q=https://079xk.mjt.lu/lnk/AU4AAFT1m8YAAAAAAAAAAlmz1CcAAYCtSIUAAAAAACPUeABnKdnNbMFetHGyQ9ursJAD3kGn5wAhkNU/7/dNuKqBpwdnPzZqmaJqBMLg/aHR0cHM6Ly90d2l0dGVyLmNvbS9VbHRpbW9Xb1c&amp;source=gmail&amp;ust=1731498433469000&amp;usg=AOvVaw0mNhdOEFArJFuU_zdxLrVr"><img title="Twitter" src="https://ci3.googleusercontent.com/meips/ADKq_Na3G-i__xB8LyVCx9Wxs_nsW07gi3Uo3-27M3uXViT9DnhI0oykXfumzibiio1R0jLpHVKx3W1Yao5mcAODWc-6LS0nx9nuKJ2_L7qY6eoUhfdF-VxxWeEQR1LBwnsjowmhy83_izS-=s0-d-e1-ft#{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-mails/twitter-logo-gray.png" alt="Tw" width="32" height="32" style="display:block;border:0;outline:none;text-decoration:none" class="CToWUd" data-bit="iit"></a></td>
<td valign="top" align="center" style="padding:0;Margin:0"><a href="https://079xk.mjt.lu/lnk/AU4AAFT1m8YAAAAAAAAAAlmz1CcAAYCtSIUAAAAAACPUeABnKdnNbMFetHGyQ9ursJAD3kGn5wAhkNU/8/sGLIpTKAAbVLc-tQyYqAVQ/aHR0cHM6Ly93d3cueW91dHViZS5jb20vY2hhbm5lbC9VQ2FITDhCWmNobzhBa2VNOXdja1FieWc" style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';font-size:14px;text-decoration:underline;color:#333333" target="_blank" data-saferedirecturl="https://www.google.com/url?q=https://079xk.mjt.lu/lnk/AU4AAFT1m8YAAAAAAAAAAlmz1CcAAYCtSIUAAAAAACPUeABnKdnNbMFetHGyQ9ursJAD3kGn5wAhkNU/8/sGLIpTKAAbVLc-tQyYqAVQ/aHR0cHM6Ly93d3cueW91dHViZS5jb20vY2hhbm5lbC9VQ2FITDhCWmNobzhBa2VNOXdja1FieWc&amp;source=gmail&amp;ust=1731498433469000&amp;usg=AOvVaw09YlWuRMpxVueGWvjg9E08"><img title="Youtube" src="https://ci3.googleusercontent.com/meips/ADKq_NYxr7vXp62rdfUJTmC76VPL7ErYx7DTXmdpM3gt6ghQ1Ag6E9aLTkja5Jgff_A5020Lrm7K20cjG2RjHCtGTSd2DWxmCD4XvB5VEDXXyL_ivnQBG-hXm42mKYqlBHYFHKwEr9p44lIt=s0-d-e1-ft#{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-mails/youtube-logo-gray.png" alt="Yt" width="32" height="32" style="display:block;border:0;outline:none;text-decoration:none" class="CToWUd" data-bit="iit"></a></td>
</tr>
</tbody></table></td>
</tr>
</tbody></table></td>
</tr>
</tbody></table></td>
</tr>
<tr style="border-collapse:collapse">
<td style="padding:0;Margin:0" align="left">
<table width="100%" cellspacing="0" cellpadding="0" style="border-collapse:collapse;border-spacing:0px">
<tbody><tr style="border-collapse:collapse">
<td valign="top" align="center" style="padding:0;Margin:0;width:600px">
<table width="100%" cellspacing="0" cellpadding="0" background="{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-mails/nw-mail-body.jpg#{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-mails/nw-mail-body.jpg" style="border-collapse:collapse;border-spacing:0px;background-image:url({{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-mails/nw-mail-body.jpg#{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-mails/nw-mail-body.jpg);background-repeat:no-repeat;background-position:left top" role="presentation">
<tbody><tr style="border-collapse:collapse">
<td align="center" style="padding:0;Margin:0;padding-top:10px;padding-bottom:15px"><p style="Margin:0;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';line-height:21px;color:#5c4c44"><a href="https://079xk.mjt.lu/lnk/AU4AAFT1m8YAAAAAAAAAAlmz1CcAAYCtSIUAAAAAACPUeABnKdnNbMFetHGyQ9ursJAD3kGn5wAhkNU/9/ZlWuOVIbPYaleSpsSuw3Ew/aHR0cHM6Ly91bHRpbW93b3cuY29tL2VzL3Rlcm1zLWFuZC1jb25kaXRpb25z" style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';font-size:14px;text-decoration:none;color:#2471a9" target="_blank" data-saferedirecturl="https://www.google.com/url?q=https://079xk.mjt.lu/lnk/AU4AAFT1m8YAAAAAAAAAAlmz1CcAAYCtSIUAAAAAACPUeABnKdnNbMFetHGyQ9ursJAD3kGn5wAhkNU/9/ZlWuOVIbPYaleSpsSuw3Ew/aHR0cHM6Ly91bHRpbW93b3cuY29tL2VzL3Rlcm1zLWFuZC1jb25kaXRpb25z&amp;source=gmail&amp;ust=1731498433469000&amp;usg=AOvVaw2dsWhHN3CcN0kKQnyUm91o">Términos y Condiciones</a>&nbsp;| <a href="https://079xk.mjt.lu/lnk/AU4AAFT1m8YAAAAAAAAAAlmz1CcAAYCtSIUAAAAAACPUeABnKdnNbMFetHGyQ9ursJAD3kGn5wAhkNU/10/5L1HSlG1_MryxhAwLok8Hw/aHR0cHM6Ly91bHRpbW93b3cuY29tL2VzL3JlZnVuZC1wb2xpY3k" style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';font-size:14px;text-decoration:none;color:#2471a9" target="_blank" data-saferedirecturl="https://www.google.com/url?q=https://079xk.mjt.lu/lnk/AU4AAFT1m8YAAAAAAAAAAlmz1CcAAYCtSIUAAAAAACPUeABnKdnNbMFetHGyQ9ursJAD3kGn5wAhkNU/10/5L1HSlG1_MryxhAwLok8Hw/aHR0cHM6Ly91bHRpbW93b3cuY29tL2VzL3JlZnVuZC1wb2xpY3k&amp;source=gmail&amp;ust=1731498433469000&amp;usg=AOvVaw0pVCr9IINwuwAKBTAXvMid">Política de Reembolso</a>&nbsp;| <a href="https://079xk.mjt.lu/lnk/AU4AAFT1m8YAAAAAAAAAAlmz1CcAAYCtSIUAAAAAACPUeABnKdnNbMFetHGyQ9ursJAD3kGn5wAhkNU/11/bmmVW3qrlVYp2WbjghJdCQ/aHR0cHM6Ly91bHRpbW93b3cuY29tL2VzL3ByaXZhY3ktcG9saWN5" style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';font-size:14px;text-decoration:none;color:#2471a9" target="_blank" data-saferedirecturl="https://www.google.com/url?q=https://079xk.mjt.lu/lnk/AU4AAFT1m8YAAAAAAAAAAlmz1CcAAYCtSIUAAAAAACPUeABnKdnNbMFetHGyQ9ursJAD3kGn5wAhkNU/11/bmmVW3qrlVYp2WbjghJdCQ/aHR0cHM6Ly91bHRpbW93b3cuY29tL2VzL3ByaXZhY3ktcG9saWN5&amp;source=gmail&amp;ust=1731498433469000&amp;usg=AOvVaw3DoZqMDsaHsX7G4FIF7Fqy">Política de Privacidad</a><br><span class="il">{{ NOMBRE_SERVIDOR }}</span> © {{ ANIO_ACTUAL }}</p></td>
</tr>
</tbody></table></td>
</tr>
</tbody></table></td>
</tr>
</tbody></table></td>
</tr>
</tbody></table></td>
@@ -1,28 +0,0 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Activación del Nuevo Correo</title>
</head>
<body>
<table style="width: 100%; max-width: 600px; margin: 0 auto; border: 1px solid #dddddd; padding: 20px;">
<tr>
<td style="text-align: center;">
<h2>Activación de Nuevo Correo</h2>
</td>
</tr>
<tr>
<td>
<p>Hola {{ username }},</p>
<p>Tu solicitud para cambiar el correo electrónico en <strong>{{ NOMBRE_SERVIDOR }}</strong> ha sido recibida.</p>
<p>Por favor, haz clic en el siguiente enlace para confirmar tu nuevo correo:</p>
<p style="text-align: center;">
<a href="{{ activation_link }}" style="display: inline-block; padding: 10px 20px; background-color: #28a745; color: white; text-decoration: none; border-radius: 5px;">Activar nuevo correo</a>
</p>
<p>Si no realizaste esta solicitud, por favor contacta con soporte.</p>
<p>Atentamente,<br>El equipo de {{ NOMBRE_SERVIDOR }}</p>
</td>
</tr>
</table>
</body>
</html>
@@ -1,28 +0,0 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Confirmación de Cambio de Correo</title>
</head>
<body>
<table style="width: 100%; max-width: 600px; margin: 0 auto; border: 1px solid #dddddd; padding: 20px;">
<tr>
<td style="text-align: center;">
<h2>Confirmación de Cambio de Correo</h2>
</td>
</tr>
<tr>
<td>
<p>Hola {{ username }},</p>
<p>Has solicitado cambiar tu correo electrónico registrado en <strong>{{ NOMBRE_SERVIDOR }}</strong>.</p>
<p>Para confirmar este cambio, haz clic en el siguiente enlace:</p>
<p style="text-align: center;">
<a href="{{ confirm_link }}" style="display: inline-block; padding: 10px 20px; background-color: #007bff; color: white; text-decoration: none; border-radius: 5px;">Confirmar cambio</a>
</p>
<p>Si no solicitaste este cambio, por favor ignora este correo.</p>
<p>Atentamente,<br>El equipo de {{ NOMBRE_SERVIDOR }}</p>
</td>
</tr>
</table>
</body>
</html>
@@ -1,27 +0,0 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Notificación de Cambio de Correo</title>
</head>
<body>
<table style="width: 100%; max-width: 600px; margin: 0 auto; border: 1px solid #dddddd; padding: 20px;">
<tr>
<td style="text-align: center;">
<h2>Cambio de Correo Exitoso</h2>
</td>
</tr>
<tr>
<td>
<p>Hola {{ username }},</p>
<p>Te notificamos que tu correo electrónico en <strong>{{ NOMBRE_SERVIDOR }}</strong> ha sido cambiado exitosamente al siguiente correo:</p>
<p style="text-align: center;">
<strong>{{ new_email }}</strong>
</p>
<p>Si no realizaste este cambio, por favor contacta inmediatamente con soporte.</p>
<p>Atentamente,<br>El equipo de {{ NOMBRE_SERVIDOR }}</p>
</td>
</tr>
</table>
</body>
</html>
-29
View File
@@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="es">
<head><meta charset="utf-8"></head>
<body style="margin:0;padding:0;background:#1b120b;font-family:Arial,Helvetica,sans-serif;color:#e8dccb;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#1b120b;padding:24px 0;">
<tr>
<td align="center">
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:#241812;border:1px solid #4a3320;border-radius:8px;">
<tr>
<td style="padding:28px 32px;">
<h1 style="margin:0 0 8px;font-size:20px;color:#d79602;">{{ NOMBRE_SERVIDOR }}</h1>
<h2 style="margin:0 0 16px;font-size:16px;color:#e8dccb;">Restablecer contraseña</h2>
<p style="margin:0 0 12px;line-height:1.5;">Hola,</p>
<p style="margin:0 0 12px;line-height:1.5;">Hemos recibido una solicitud para restablecer la contraseña de la cuenta Battle.net asociada a <strong>{{ email }}</strong>.</p>
<p style="margin:0 0 20px;line-height:1.5;">Pulsa el botón para elegir una nueva contraseña. El enlace caduca en 1 hora.</p>
<p style="margin:0 0 20px;">
<a href="{{ reset_link }}" style="display:inline-block;padding:12px 22px;background:#d79602;color:#1b120b;text-decoration:none;border-radius:6px;font-weight:bold;">Restablecer contraseña</a>
</p>
<p style="margin:0 0 8px;line-height:1.5;font-size:13px;color:#b1997f;">Si el botón no funciona, copia esta dirección en tu navegador:</p>
<p style="margin:0 0 20px;line-height:1.5;font-size:13px;word-break:break-all;"><a href="{{ reset_link }}" style="color:#2471a9;">{{ reset_link }}</a></p>
<p style="margin:0;line-height:1.5;font-size:13px;color:#b1997f;">Si no has solicitado este cambio, ignora este correo: tu contraseña no se modificará.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More