From 91c737c907f174b5252877876126a8b81e6cb831 Mon Sep 17 00:00:00 2001
From: Pinapelz <yukais@pinapelz.com>
Date: Fri, 7 Nov 2025 22:46:34 -0800
Subject: add support to limit registration to invite codes

---
 frontend/src/pages/Admin.tsx | 164 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 164 insertions(+)

(limited to 'frontend/src/pages/Admin.tsx')

diff --git a/frontend/src/pages/Admin.tsx b/frontend/src/pages/Admin.tsx
index f494fc2..043a32e 100644
--- a/frontend/src/pages/Admin.tsx
+++ b/frontend/src/pages/Admin.tsx
@@ -8,12 +8,19 @@ import { useState } from "react";
 const Admin = () => {
   const { user, isLoading, logout } = useAuth();
   const [showAddGame, setShowAddGame] = useState(false);
+  const [showCreateInvite, setShowCreateInvite] = useState(false);
   const [formData, setFormData] = useState({
     gameInternalName: '',
     gameFormattedName: '',
     gameDescription: ''
   });
+  const [inviteFormData, setInviteFormData] = useState({
+    uses: '',
+    code: ''
+  });
   const [isSubmitting, setIsSubmitting] = useState(false);
+  const [isCreatingInvite, setIsCreatingInvite] = useState(false);
+  const [createdInviteCode, setCreatedInviteCode] = useState<string | null>(null);
   const navigate = useNavigate();
 
   const handleLogout = async () => {
@@ -34,6 +41,14 @@ const Admin = () => {
     }));
   };
 
+  const handleInviteInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const { name, value } = e.target;
+    setInviteFormData(prev => ({
+      ...prev,
+      [name]: value
+    }));
+  };
+
   const handleSubmit = async (e: React.FormEvent) => {
     e.preventDefault();
 
@@ -75,6 +90,65 @@ const Admin = () => {
     }
   };
 
+  const handleInviteSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+
+    if (!inviteFormData.uses) {
+      alert('Please specify the number of uses for the invite code');
+      return;
+    }
+
+    const uses = parseInt(inviteFormData.uses);
+    if (isNaN(uses) || uses <= 0) {
+      alert('Please enter a valid number of uses');
+      return;
+    }
+
+    setIsCreatingInvite(true);
+
+    try {
+      const requestBody: { uses: number; code?: string } = { uses };
+      if (inviteFormData.code.trim()) {
+        requestBody.code = inviteFormData.code.trim();
+      }
+
+      const response = await fetch(import.meta.env.VITE_API_URL + '/admin/createInvite', {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        credentials: 'include',
+        body: JSON.stringify(requestBody),
+      });
+
+      if (!response.ok) {
+        const error = await response.json();
+        throw new Error(error.error || 'Failed to create invite code');
+      }
+
+      const result = await response.json();
+      setCreatedInviteCode(result.inviteCode.code);
+      setInviteFormData({
+        uses: '',
+        code: ''
+      });
+
+    } catch (error) {
+      console.error('Failed to create invite code:', error);
+      alert(error instanceof Error ? error.message : 'Failed to create invite code');
+    } finally {
+      setIsCreatingInvite(false);
+    }
+  };
+
+  const copyToClipboard = (text: string) => {
+    navigator.clipboard.writeText(text).then(() => {
+      alert('Invite code copied to clipboard!');
+    }).catch(() => {
+      alert('Failed to copy to clipboard');
+    });
+  };
+
   if (isLoading) {
     return (
       <div className="min-h-screen bg-slate-950 flex items-center justify-center">
@@ -112,6 +186,96 @@ const Admin = () => {
             Welcome Mirage Webmaster! Here are a variety of settings and tools you can use to customize the experience
           </p>
         </div>
+
+        {/* Create Invite Code Section */}
+        <div className="mb-8">
+          <div className="bg-slate-900 rounded-lg border border-slate-700">
+            <button
+              className="w-full px-6 py-4 text-left flex items-center justify-between hover:bg-slate-800 transition-colors rounded-lg"
+              onClick={() => setShowCreateInvite(!showCreateInvite)}
+            >
+              <h2 className="text-xl font-semibold text-white">Create Invite Code</h2>
+              <svg
+                className={`w-5 h-5 text-slate-400 transform transition-transform ${
+                  showCreateInvite ? 'rotate-180' : ''
+                }`}
+                fill="none"
+                stroke="currentColor"
+                viewBox="0 0 24 24"
+              >
+                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
+              </svg>
+            </button>
+            {showCreateInvite && (
+              <div className="px-6 pb-6">
+                <p className="text-slate-300 leading-relaxed mb-6 p-4 bg-slate-800/50 rounded-md border-l-4 border-violet-500">
+                  Generate invite codes to allow new users to register. You can specify how many times the code can be used
+                  and optionally set a custom code (otherwise one will be generated automatically).
+                </p>
+
+                {createdInviteCode && (
+                  <div className="mb-6 p-4 bg-green-900/30 border border-green-700 rounded-md">
+                    <h3 className="text-green-400 font-semibold mb-2">Invite Code Created Successfully!</h3>
+                    <div className="flex items-center gap-2">
+                      <code className="bg-slate-800 px-3 py-2 rounded text-green-300 font-mono">
+                        {createdInviteCode}
+                      </code>
+                      <button
+                        onClick={() => copyToClipboard(createdInviteCode)}
+                        className="bg-green-600 hover:bg-green-700 text-white px-3 py-2 rounded-md text-sm transition-colors"
+                      >
+                        Copy
+                      </button>
+                    </div>
+                  </div>
+                )}
+
+                <form className="space-y-4" onSubmit={handleInviteSubmit}>
+                  <div>
+                    <label htmlFor="uses" className="block text-sm font-medium text-slate-300 mb-2">
+                      Number of Uses
+                    </label>
+                    <input
+                      type="number"
+                      id="uses"
+                      name="uses"
+                      value={inviteFormData.uses}
+                      onChange={handleInviteInputChange}
+                      className="w-full px-3 py-2 bg-slate-800 border border-slate-600 rounded-md text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-transparent"
+                      placeholder="How many times this code can be used"
+                      min="1"
+                      required
+                    />
+                  </div>
+                  <div>
+                    <label htmlFor="code" className="block text-sm font-medium text-slate-300 mb-2">
+                      Custom Code (Optional)
+                    </label>
+                    <input
+                      type="text"
+                      id="code"
+                      name="code"
+                      value={inviteFormData.code}
+                      onChange={handleInviteInputChange}
+                      className="w-full px-3 py-2 bg-slate-800 border border-slate-600 rounded-md text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-transparent"
+                      placeholder="Leave blank to generate automatically"
+                    />
+                  </div>
+                  <div className="pt-4">
+                    <button
+                      type="submit"
+                      disabled={isCreatingInvite}
+                      className="bg-violet-600 hover:bg-violet-700 disabled:bg-violet-800 disabled:cursor-not-allowed text-white font-medium py-2 px-4 rounded-md transition-colors"
+                    >
+                      {isCreatingInvite ? 'Creating Invite Code...' : 'Create Invite Code'}
+                    </button>
+                  </div>
+                </form>
+              </div>
+            )}
+          </div>
+        </div>
+
         {/* Add New Game Section */}
         <div className="mb-8">
           <div className="bg-slate-900 rounded-lg border border-slate-700">
-- 
cgit v1.2.3

