Add invite request system and Gitea Actions CI/CD pipeline
Some checks failed
Build & Deploy Staging / build (release) Waiting to run
Build & Deploy Staging / deploy (release) Has been cancelled

Invite request feature:
- Public form to request an invite when INVITE_REQUEST_ENABLED=true
- Stores requests in new invite_requests DB table
- Emails admins on new request, emails requester on approve/deny
- Admin panel tab to review, approve, and deny requests
- Approval auto-creates invite code and sends signup link

CI/CD pipeline:
- Build & push Docker image to Gitea registry on release
- Auto-deploy to staging with health check
- Manual workflow_dispatch for production deploys

Also includes client layout/sizing improvements for card grid
and opponent spacing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-07 19:38:52 -04:00
parent 0c0588f920
commit ef54ac201a
16 changed files with 1003 additions and 50 deletions

View File

@@ -1,4 +1,3 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Golf Card Game - Client Application
// Debug logging - set to true to see detailed state/animation logs
@@ -4868,6 +4867,15 @@ class AuthManager {
this.resetPasswordConfirm = document.getElementById('reset-password-confirm');
this.resetError = document.getElementById('reset-error');
this.resetSuccess = document.getElementById('reset-success');
this.requestInviteContainer = document.getElementById('request-invite-container');
this.requestInviteForm = document.getElementById('request-invite-form');
this.requestInviteName = document.getElementById('request-invite-name');
this.requestInviteEmail = document.getElementById('request-invite-email');
this.requestInviteMessage = document.getElementById('request-invite-message');
this.requestInviteError = document.getElementById('request-invite-error');
this.requestInviteSuccess = document.getElementById('request-invite-success');
this.requestBackSignup = document.getElementById('request-back-signup');
this.requestBackLogin = document.getElementById('request-back-login');
}
bindEvents() {
@@ -4898,6 +4906,15 @@ class AuthManager {
});
this.forgotForm?.addEventListener('submit', (e) => this.handleForgotPassword(e));
this.resetForm?.addEventListener('submit', (e) => this.handleResetPassword(e));
this.requestInviteForm?.addEventListener('submit', (e) => this.handleRequestInvite(e));
this.requestBackSignup?.addEventListener('click', (e) => {
e.preventDefault();
this.showForm('signup');
});
this.requestBackLogin?.addEventListener('click', (e) => {
e.preventDefault();
this.showForm('login');
});
// Check URL for reset token or invite code on page load
this.checkResetToken();
@@ -4923,6 +4940,7 @@ class AuthManager {
this.signupFormContainer.classList.add('hidden');
this.forgotFormContainer?.classList.add('hidden');
this.resetFormContainer?.classList.add('hidden');
this.requestInviteContainer?.classList.add('hidden');
this.clearErrors();
if (form === 'login') {
@@ -4937,6 +4955,9 @@ class AuthManager {
} else if (form === 'reset') {
this.resetFormContainer?.classList.remove('hidden');
this.resetPassword?.focus();
} else if (form === 'request-invite') {
this.requestInviteContainer?.classList.remove('hidden');
this.requestInviteName?.focus();
}
}
@@ -4953,6 +4974,8 @@ class AuthManager {
if (this.forgotSuccess) this.forgotSuccess.textContent = '';
if (this.resetError) this.resetError.textContent = '';
if (this.resetSuccess) this.resetSuccess.textContent = '';
if (this.requestInviteError) this.requestInviteError.textContent = '';
if (this.requestInviteSuccess) this.requestInviteSuccess.textContent = '';
}
async handleLogin(e) {
@@ -5089,7 +5112,17 @@ class AuthManager {
if (invite_required) {
this.signupInviteCode.required = true;
this.signupInviteCode.placeholder = 'Invite Code (required)';
if (this.inviteCodeHint) this.inviteCodeHint.textContent = '';
if (this.inviteCodeHint) {
if (this.signupInfo.invite_request_enabled) {
this.inviteCodeHint.innerHTML = 'Don\'t have one? <a href="#" id="show-request-invite">Request an invite</a>';
document.getElementById('show-request-invite')?.addEventListener('click', (e) => {
e.preventDefault();
this.showForm('request-invite');
});
} else {
this.inviteCodeHint.textContent = '';
}
}
} else if (open_signups_enabled) {
this.signupInviteCode.required = false;
this.signupInviteCode.placeholder = 'Invite Code (optional)';
@@ -5166,4 +5199,33 @@ class AuthManager {
this.resetError.textContent = 'Connection error';
}
}
async handleRequestInvite(e) {
e.preventDefault();
this.clearErrors();
const name = this.requestInviteName.value.trim();
const email = this.requestInviteEmail.value.trim();
const message = this.requestInviteMessage.value.trim() || null;
try {
const response = await fetch('/api/auth/request-invite', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, message }),
});
const data = await response.json();
if (!response.ok) {
this.requestInviteError.textContent = data.detail || 'Request failed';
return;
}
this.requestInviteSuccess.textContent = data.message;
this.requestInviteForm.reset();
} catch (err) {
this.requestInviteError.textContent = 'Connection error';
}
}
}