Files
edh-stats/frontend/public/register.html
Michael Skrynski a2b9827279 Fix security vulnerability: validate auth tokens with backend
SECURITY FIX:
- Removed unsafe client-side only token check
- Added server-side token validation via /api/auth/me endpoint
- Prevents tokens spoofed in localStorage from granting access
- Only redirects if token is verified as valid by backend

How it works:
- When user visits login/register page with token in storage
- auth-check.js makes API call to /api/auth/me with token
- Backend JWT middleware verifies token signature and expiration
- If valid, user is redirected to dashboard
- If invalid/expired, token is cleared and user stays on login page
- If network error, user stays on login page (no redirect)
2026-01-23 09:45:34 +01:00

726 lines
28 KiB
HTML

<!doctype html>
<html lang="en" class="h-full bg-gray-50">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Register - EDH Stats Tracker</title>
<meta
name="description"
content="Create a new account to track your Magic: The Gathering EDH/Commander games"
/>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body class="min-h-full flex flex-col py-12 px-4 sm:px-6 lg:px-8" x-data="{ showTosModal: false }">
<div class="flex items-center justify-center flex-1">
<div
class="max-w-md w-full space-y-8"
x-data="registerForm()"
x-init="init()"
>
<!-- Header -->
<div class="text-center">
<h1 class="text-4xl font-bold font-mtg text-edh-primary mb-2">
EDH Stats
</h1>
<h2
class="text-xl text-gray-600"
x-text="allowRegistration ? 'Create your account' : 'Registration Disabled'"
></h2>
</div>
<!-- Registration Disabled Message -->
<div
x-show="!allowRegistration"
x-transition
class="card bg-yellow-50 border-yellow-200"
>
<div class="flex">
<div class="flex-shrink-0">
<svg
class="h-5 w-5 text-yellow-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-yellow-800">
User registration is currently disabled. Please contact an
administrator if you need to create an account.
</p>
</div>
</div>
<div class="mt-6 text-center">
<p class="text-sm text-gray-600">
If you already have an account,
<a
href="/login.html"
class="font-medium text-edh-accent hover:text-edh-primary"
>
sign in here
</a>
</p>
</div>
</div>
<!-- Register Form -->
<div class="card" x-show="allowRegistration">
<form class="space-y-6" @submit.prevent="handleRegister">
<!-- Username Field -->
<div>
<label for="username" class="form-label">Username</label>
<div class="relative">
<input
id="username"
name="username"
type="text"
required
x-model="formData.username"
@input="validateUsername()"
:class="errors.username ? 'border-red-500 focus:ring-red-500' : ''"
class="form-input pl-10"
placeholder="Choose a username (3+ characters)"
/>
<div
class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"
>
<svg
class="h-5 w-5 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
></path>
</svg>
</div>
</div>
<p
x-show="errors.username"
x-text="errors.username"
class="form-error"
></p>
</div>
<!-- Email Field -->
<div>
<label for="email" class="form-label"
>Email Address (Optional)</label
>
<div class="relative">
<input
id="email"
name="email"
type="email"
x-model="formData.email"
@input="validateEmail()"
:class="errors.email ? 'border-red-500 focus:ring-red-500' : ''"
class="form-input pl-10"
placeholder="Enter your email"
/>
<div
class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"
>
<svg
class="h-5 w-5 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
></path>
</svg>
</div>
</div>
<p
x-show="errors.email"
x-text="errors.email"
class="form-error"
></p>
</div>
<!-- Password Field -->
<div>
<label for="password" class="form-label">Password</label>
<div class="relative">
<input
id="password"
name="password"
:type="showPassword ? 'text' : 'password'"
required
x-model="formData.password"
@input="validatePassword()"
:class="errors.password ? 'border-red-500 focus:ring-red-500' : ''"
class="form-input pl-10 pr-10"
placeholder="Minimum 8 characters"
/>
<div
class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"
>
<svg
class="h-5 w-5 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
></path>
</svg>
</div>
<button
type="button"
@click="showPassword = !showPassword"
class="absolute inset-y-0 right-0 pr-3 flex items-center"
>
<svg
x-show="!showPassword"
class="h-5 w-5 text-gray-400 hover:text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
></path>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
></path>
</svg>
<svg
x-show="showPassword"
class="h-5 w-5 text-gray-400 hover:text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
></path>
</svg>
</button>
</div>
<p
x-show="errors.password"
x-text="errors.password"
class="form-error"
></p>
</div>
<!-- Confirm Password Field -->
<div>
<label for="confirmPassword" class="form-label"
>Confirm Password</label
>
<div class="relative">
<input
id="confirmPassword"
name="confirmPassword"
:type="showConfirmPassword ? 'text' : 'password'"
required
x-model="formData.confirmPassword"
@input="validateConfirmPassword()"
:class="errors.confirmPassword ? 'border-red-500 focus:ring-red-500' : ''"
class="form-input pl-10 pr-10"
placeholder="Re-enter your password"
/>
<div
class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"
>
<svg
class="h-5 w-5 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
></path>
</svg>
</div>
<button
type="button"
@click="showConfirmPassword = !showConfirmPassword"
class="absolute inset-y-0 right-0 pr-3 flex items-center"
>
<svg
x-show="!showConfirmPassword"
class="h-5 w-5 text-gray-400 hover:text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
></path>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
></path>
</svg>
<svg
x-show="showConfirmPassword"
class="h-5 w-5 text-gray-400 hover:text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
></path>
</svg>
</button>
</div>
<p
x-show="errors.confirmPassword"
x-text="errors.confirmPassword"
class="form-error"
></p>
</div>
<!-- Terms & Conditions -->
<div class="flex items-center">
<input
id="terms"
name="terms"
type="checkbox"
x-model="formData.terms"
@change="validateTerms()"
:class="errors.terms ? 'border-red-500' : ''"
class="h-4 w-4 text-edh-accent focus:ring-edh-accent border-gray-300 rounded"
/>
<label for="terms" class="ml-2 block text-sm text-gray-900">
I agree to the
<button
type="button"
@click="showTosModal = true"
class="text-edh-accent hover:text-edh-primary font-medium"
>
Terms of Service
</button>
</label>
</div>
<p
x-show="errors.terms"
x-text="errors.terms"
class="form-error"
></p>
<!-- Error Message -->
<div
x-show="serverError"
x-transition
class="rounded-md bg-red-50 p-4"
>
<div class="flex">
<div class="flex-shrink-0">
<svg
class="h-5 w-5 text-red-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-red-800" x-text="serverError"></p>
</div>
</div>
</div>
<!-- Success Message -->
<div
x-show="successMessage"
x-transition
class="rounded-md bg-green-50 p-4"
>
<div class="flex">
<div class="flex-shrink-0">
<svg
class="h-5 w-5 text-green-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-green-800" x-text="successMessage"></p>
</div>
</div>
</div>
<!-- Submit Button -->
<div>
<button
type="submit"
:disabled="loading"
class="btn btn-primary w-full flex justify-center items-center space-x-2"
:class="{ 'opacity-50 cursor-not-allowed': loading }"
>
<svg
x-show="!loading"
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"
></path>
</svg>
<svg
x-show="loading"
class="animate-spin h-5 w-5"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<span
x-text="loading ? 'Creating account...' : 'Create account'"
></span>
</button>
</div>
<!-- Login Link -->
<div class="text-center">
<p class="text-sm text-gray-600">
Already have an account?
<a
href="/login.html"
class="font-medium text-edh-accent hover:text-edh-primary"
>
Sign in
</a>
</p>
</div>
</form>
</div>
<!-- Password Requirements Info -->
<div class="card bg-blue-50 border-blue-200" x-show="allowRegistration">
<h3 class="text-sm font-medium text-blue-800 mb-2">
Password Requirements
</h3>
<div class="text-xs text-blue-700 space-y-1">
<p
class="flex items-center"
:class="{ 'text-green-700': formData.password.length >= 8 }"
>
<svg
:class="{ 'text-green-500': formData.password.length >= 8 }"
class="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
></path>
</svg>
At least 8 characters
</p>
<p
class="flex items-center"
:class="{ 'text-green-700': /(?=.*[a-z])/.test(formData.password) }"
>
<svg
:class="{ 'text-green-500': /(?=.*[a-z])/.test(formData.password) }"
class="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
></path>
</svg>
At least one lowercase letter
</p>
<p
class="flex items-center"
:class="{ 'text-green-700': /(?=.*[A-Z])/.test(formData.password) }"
>
<svg
:class="{ 'text-green-500': /(?=.*[A-Z])/.test(formData.password) }"
class="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
></path>
</svg>
At least one uppercase letter
</p>
<p
class="flex items-center"
:class="{ 'text-green-700': /(?=.*\d)/.test(formData.password) }"
>
<svg
:class="{ 'text-green-500': /(?=.*\d)/.test(formData.password) }"
class="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
></path>
</svg>
At least one number (0-9)
</p>
</div>
</div>
</div>
</div>
<!-- Terms of Service Modal -->
<template x-if="showTosModal">
<div class="fixed inset-0 bg-black bg-opacity-50 z-40" @click="showTosModal = false"></div>
</template>
<template x-if="showTosModal">
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-lg shadow-xl w-full h-[90vh] flex flex-col lg:w-1/2 sm:w-11/12">
<!-- Modal Header -->
<div class="flex-shrink-0 bg-white border-b border-gray-200 p-6 flex justify-between items-center">
<h2 class="text-2xl font-bold font-mtg text-edh-primary">
Terms of Service
</h2>
<button
type="button"
@click="showTosModal = false"
class="text-gray-400 hover:text-gray-600 flex-shrink-0"
>
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>
</button>
</div>
<!-- Modal Content - Scrollable -->
<div class="flex-1 overflow-y-auto p-6 text-gray-700">
<p class="text-gray-600 mb-6 text-sm">Last updated: January 2026</p>
<div class="prose prose-sm max-w-none">
<h2 class="text-2xl font-bold text-gray-900 mt-6 mb-4">
Welcome to EDH Stats Tracker
</h2>
<p class="text-gray-700 mb-4">
By creating an account and using EDH Stats Tracker, you agree to these Terms of Service.
We've kept them simple and straightforward—no legal jargon that makes your brain hurt. (You're welcome.)
</p>
<h2 class="text-2xl font-bold text-gray-900 mt-6 mb-4">
1. What This Service Is
</h2>
<p class="text-gray-700 mb-4">
EDH Stats Tracker is a web application designed to help Magic: The Gathering players track,
analyze, and celebrate their EDH/Commander game statistics. We store your game records,
commanders, and associated statistics. Think of us as your personal game journal that actually does math for you.
</p>
<h2 class="text-2xl font-bold text-gray-900 mt-6 mb-4">
2. User Accounts
</h2>
<p class="text-gray-700 mb-4">
You are responsible for maintaining the confidentiality of your password. You agree not to share your account
credentials with anyone else. If someone logs into your account and logs all your games as losses, we'll sympathize,
but that's on you.
</p>
<p class="text-gray-700 mb-4">
You represent that the information you provide during registration is accurate and true.
If you use a fake name, that's between you and Magic's lore team.
</p>
<h2 class="text-2xl font-bold text-gray-900 mt-6 mb-4">
3. Your Content
</h2>
<p class="text-gray-700 mb-4">
All game records, commander lists, notes, and data you enter into EDH Stats Tracker remain your property.
We don't own your stats—we just help you organize them. We won't sell your data, trade it for pack equity, or share it with strangers.
(We're not monsters.)
</p>
<h2 class="text-2xl font-bold text-gray-900 mt-6 mb-4">
4. Acceptable Use
</h2>
<p class="text-gray-700 mb-4">
You agree to use EDH Stats Tracker for its intended purpose: tracking and analyzing your EDH games.
Don't use it to harass, deceive, or cause harm to others. Be cool.
</p>
<p class="text-gray-700 mb-4">
Don't try to break the service through hacking, automated attacks, or other malicious means.
If you find a security vulnerability, please let us know responsibly instead.
</p>
<h2 class="text-2xl font-bold text-gray-900 mt-6 mb-4">
5. Service Availability
</h2>
<p class="text-gray-700 mb-4">
We aim to keep EDH Stats Tracker available and reliable. However, like all software,
it may occasionally go down for maintenance or experience technical issues. We're doing our best here.
</p>
<p class="text-gray-700 mb-4">
We reserve the right to make changes to the service, add features, or modify functionality
as we see fit. We'll try to keep breaking changes to a minimum.
</p>
<h2 class="text-2xl font-bold text-gray-900 mt-6 mb-4">
6. Limitation of Liability
</h2>
<p class="text-gray-700 mb-4">
EDH Stats Tracker is provided "as is." While we work hard to make it great,
we don't guarantee it will be perfect or meet every need. We're not liable for data loss,
lost wins, or your opponent's lucky top-decks.
</p>
<h2 class="text-2xl font-bold text-gray-900 mt-6 mb-4">
7. Changes to Terms
</h2>
<p class="text-gray-700 mb-4">
We may update these Terms of Service from time to time. We'll let you know about significant changes.
Your continued use of the service after changes means you accept the new terms.
</p>
<h2 class="text-2xl font-bold text-gray-900 mt-6 mb-4">
8. Account Termination
</h2>
<p class="text-gray-700 mb-4">
You can delete your account at any time. Your data will be removed from our systems
in accordance with our privacy practices. If you violate these terms, we may disable your account.
</p>
<h2 class="text-2xl font-bold text-gray-900 mt-6 mb-4">
9. Questions?
</h2>
<p class="text-gray-700 mb-4">
If you have questions about these terms, please reach out. We're reasonable people
(at least we think so).
</p>
<div class="mt-8 p-6 bg-blue-50 border border-blue-200 rounded-lg">
<p class="text-blue-900 text-sm">
<strong>TL;DR:</strong> Use the service as intended, keep your password safe, it's your responsibility.
We'll keep your data private and try to keep the service running. Don't be a jerk. That's it.
</p>
</div>
</div>
</div>
<!-- Modal Footer -->
<div class="flex-shrink-0 bg-gray-50 border-t border-gray-200 p-6 flex justify-between items-center">
<button
type="button"
@click="showTosModal = false"
class="px-4 py-2 text-gray-700 bg-gray-200 rounded hover:bg-gray-300"
>
Close
</button>
<button
type="button"
@click="document.getElementById('terms').checked = true; showTosModal = false"
class="px-4 py-2 text-white bg-edh-accent rounded hover:bg-edh-primary"
>
I Agree
</button>
</div>
</div>
</div>
</template>
<!-- Scripts -->
<script
defer
src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"
></script>
<script src="/js/auth.js"></script>
<script src="/js/auth-check.js"></script>
<script src="/js/footer-loader.js"></script>
</body>
</html>