Add invite request system and Gitea Actions CI/CD pipeline
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:
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user