Files
memory-infrastructure-palace/code/websites/pokedex.online/src/views/ClientCredentialsManager.vue

876 lines
20 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="client-credentials-manager">
<div class="container">
<div class="header">
<router-link to="/challonge-test" class="back-button">
Back to Challonge Test
</router-link>
<h1>Client Credentials Manager</h1>
</div>
<!-- Current Status -->
<div class="section">
<h2>Current Status</h2>
<div v-if="hasCredentials && isAuthenticated" class="status success">
<div class="status-icon"></div>
<div class="status-content">
<p><strong>Client Credentials Active</strong></p>
<p class="credentials-display">Client ID: {{ maskedClientId }}</p>
<p class="token-info">
Token expires in: {{ tokenInfo?.expiresIn || 0 }} seconds
</p>
<div class="button-group">
<button
@click="handleRefresh"
class="btn btn-primary"
:disabled="loading"
>
{{ loading ? 'Refreshing...' : 'Refresh Token' }}
</button>
<button @click="handleLogout" class="btn btn-secondary">
Logout
</button>
<button @click="showDeleteConfirm = true" class="btn btn-danger">
Clear Credentials
</button>
</div>
</div>
</div>
<div v-else-if="hasCredentials" class="status warning">
<div class="status-icon"></div>
<div class="status-content">
<p><strong>Credentials Stored (Not Authenticated)</strong></p>
<p class="credentials-display">Client ID: {{ maskedClientId }}</p>
<button
@click="handleAuthenticate"
class="btn btn-primary"
:disabled="loading"
>
{{ loading ? 'Authenticating...' : 'Get Access Token' }}
</button>
<button @click="showDeleteConfirm = true" class="btn btn-danger">
Clear Credentials
</button>
</div>
</div>
<div v-else class="status warning">
<div class="status-icon"></div>
<div class="status-content">
<p><strong>No Client Credentials Stored</strong></p>
<p>Add your OAuth client credentials below to get started</p>
</div>
</div>
</div>
<!-- Add/Update Credentials -->
<div class="section">
<div class="section-header">
<h2>{{ hasCredentials ? 'Update' : 'Add' }} Client Credentials</h2>
<button
@click="showGuide = true"
class="help-btn"
title="How to get client credentials"
>
Need Help?
</button>
</div>
<div class="form-group">
<label for="client-id">OAuth Client ID</label>
<input
id="client-id"
v-model="inputClientId"
type="text"
placeholder="Enter your OAuth Client ID"
class="form-input"
/>
</div>
<div class="form-group">
<label for="client-secret">OAuth Client Secret</label>
<div class="input-wrapper">
<input
id="client-secret"
v-model="inputClientSecret"
:type="showPassword ? 'text' : 'password'"
placeholder="Enter your OAuth Client Secret"
class="form-input"
/>
<button
@click="showPassword = !showPassword"
class="toggle-password"
:title="showPassword ? 'Hide' : 'Show'"
>
{{ showPassword ? '👁️' : '👁️‍🗨️' }}
</button>
</div>
</div>
<div class="form-group">
<label for="scope">OAuth Scope</label>
<select id="scope" v-model="selectedScope" class="form-input">
<option value="application:manage">
application:manage (Full access to app tournaments)
</option>
<option value="application:organizer">
application:organizer (User's app tournaments)
</option>
<option value="application:player">
application:player (Register and report scores)
</option>
<option value="tournaments:read tournaments:write">
tournaments:read tournaments:write (Standard access)
</option>
</select>
<p class="help-text">
<strong>application:manage</strong> is required for APPLICATION
scope tournament access
</p>
</div>
<div v-if="error" class="error-message">
{{ error }}
</div>
<button
@click="handleSaveCredentials"
:disabled="!inputClientId || !inputClientSecret || saving"
class="btn btn-primary"
>
{{
saving
? 'Saving...'
: hasCredentials
? 'Update & Authenticate'
: 'Save & Authenticate'
}}
</button>
<div v-if="successMessage" class="success-message">
{{ successMessage }}
</div>
</div>
<!-- Information -->
<div class="section info-section">
<h2> What are Client Credentials?</h2>
<div class="info-content">
<p>
<strong>Client Credentials Flow</strong> is an OAuth 2.0
authentication method for server-to-server communication. It allows
your application to access resources without user interaction.
</p>
<h3>When to use this:</h3>
<ul>
<li>
<strong>APPLICATION Scope:</strong> Access all tournaments
associated with your OAuth application
</li>
<li>
<strong>Background tasks:</strong> Automated scripts that don't
require user login
</li>
<li>
<strong>Server operations:</strong> Backend services managing
tournament data
</li>
</ul>
<h3>Security Notes:</h3>
<ul>
<li>
🔒 <strong>Keep your client secret private</strong> - Never commit
it to version control
</li>
<li>
🔐 Credentials are stored in your browser's localStorage (not sent
to any server)
</li>
<li>
⚠️ Only use on <strong>trusted devices</strong> - Clear
credentials when done
</li>
<li>
🔄 Tokens expire automatically - Use the refresh button to get a
new one
</li>
</ul>
<h3>Differences from regular OAuth:</h3>
<ul>
<li>
<strong>No user interaction:</strong> Authentication happens
automatically
</li>
<li>
<strong>Uses client secret:</strong> Requires both client ID and
secret
</li>
<li>
<strong>Limited scopes:</strong> Only works with application:*
scopes
</li>
<li>
<strong>Direct token:</strong> No authorization code exchange
needed
</li>
</ul>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div
v-if="showDeleteConfirm"
class="modal-overlay"
@click="showDeleteConfirm = false"
>
<div class="modal" @click.stop>
<h3>Delete Client Credentials?</h3>
<p>
Are you sure you want to clear the stored client credentials and
access token? You'll need to enter them again to use APPLICATION
scope.
</p>
<div class="modal-buttons">
<button
@click="showDeleteConfirm = false"
class="btn btn-secondary"
>
Cancel
</button>
<button @click="handleDeleteCredentials" class="btn btn-danger">
Delete
</button>
</div>
</div>
</div>
<!-- Help Guide Modal -->
<div v-if="showGuide" class="modal-overlay" @click="showGuide = false">
<div class="modal modal-large" @click.stop>
<div class="modal-header">
<h3>How to Get Client Credentials</h3>
<button @click="showGuide = false" class="close-btn"></button>
</div>
<div class="modal-body">
<div class="guide-step">
<div class="step-number">1</div>
<div class="step-content">
<h4>Go to Challonge Developer Settings</h4>
<p>
Visit
<a
href="https://challonge.com/settings/developer"
target="_blank"
rel="noopener"
>
https://challonge.com/settings/developer
</a>
</p>
</div>
</div>
<div class="guide-step">
<div class="step-number">2</div>
<div class="step-content">
<h4>Create or Select Your OAuth Application</h4>
<p>
If you don't have an application yet, click "Create New
Application" and fill in the details.
</p>
</div>
</div>
<div class="guide-step">
<div class="step-number">3</div>
<div class="step-content">
<h4>Copy Your Client ID and Client Secret</h4>
<p>
Your application page will show both the
<strong>Client ID</strong> and <strong>Client Secret</strong>.
Copy both values.
</p>
<p class="warning">
⚠️ <strong>Important:</strong> The client secret is only shown
once. If you lose it, you'll need to regenerate it.
</p>
</div>
</div>
<div class="guide-step">
<div class="step-number">4</div>
<div class="step-content">
<h4>Paste Credentials Here</h4>
<p>
Return to this page, paste both values, select your desired
scope, and click "Save & Authenticate".
</p>
</div>
</div>
<div class="guide-step">
<div class="step-number">5</div>
<div class="step-content">
<h4>Start Using APPLICATION Scope</h4>
<p>
Once authenticated, you can use the APPLICATION scope in the
Challonge Test page to access all tournaments associated with
your application.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useChallongeClientCredentials } from '../composables/useChallongeClientCredentials.js';
const {
hasCredentials,
maskedClientId,
isAuthenticated,
loading,
error,
tokenInfo,
saveCredentials,
clearCredentials,
authenticate,
refresh,
logout
} = useChallongeClientCredentials();
const inputClientId = ref('');
const inputClientSecret = ref('');
const selectedScope = ref('application:manage');
const showPassword = ref(false);
const showDeleteConfirm = ref(false);
const showGuide = ref(false);
const saving = ref(false);
const successMessage = ref('');
async function handleSaveCredentials() {
error.value = '';
successMessage.value = '';
// Validate input
if (!inputClientId.value.trim()) {
error.value = 'Please enter a Client ID';
return;
}
if (!inputClientSecret.value.trim()) {
error.value = 'Please enter a Client Secret';
return;
}
saving.value = true;
try {
// Save credentials
const success = saveCredentials(
inputClientId.value.trim(),
inputClientSecret.value.trim()
);
if (!success) {
error.value = 'Failed to save credentials';
return;
}
// Authenticate immediately
await authenticate(selectedScope.value);
successMessage.value =
'Client credentials saved and authenticated successfully!';
inputClientId.value = '';
inputClientSecret.value = '';
setTimeout(() => {
successMessage.value = '';
}, 3000);
} catch (err) {
error.value = err.message || 'Authentication failed';
} finally {
saving.value = false;
}
}
async function handleAuthenticate() {
error.value = '';
successMessage.value = '';
try {
await authenticate(selectedScope.value);
successMessage.value = 'Authenticated successfully!';
setTimeout(() => {
successMessage.value = '';
}, 3000);
} catch (err) {
error.value = err.message || 'Authentication failed';
}
}
async function handleRefresh() {
error.value = '';
successMessage.value = '';
try {
await refresh(selectedScope.value);
successMessage.value = 'Token refreshed successfully!';
setTimeout(() => {
successMessage.value = '';
}, 3000);
} catch (err) {
error.value = err.message || 'Token refresh failed';
}
}
function handleLogout() {
logout();
successMessage.value = 'Logged out successfully (credentials retained)';
setTimeout(() => {
successMessage.value = '';
}, 3000);
}
function handleDeleteCredentials() {
const success = clearCredentials();
if (success) {
showDeleteConfirm.value = false;
inputClientId.value = '';
inputClientSecret.value = '';
successMessage.value = 'Client credentials cleared successfully';
setTimeout(() => {
successMessage.value = '';
}, 3000);
} else {
error.value = 'Failed to clear client credentials';
}
}
</script>
<style scoped>
.client-credentials-manager {
min-height: 100vh;
padding: 2rem 1rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
max-width: 900px;
margin: 0 auto;
background: white;
border-radius: 12px;
padding: 2rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.back-button {
padding: 0.5rem 1rem;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 6px;
font-weight: 600;
transition: all 0.3s ease;
}
.back-button:hover {
background: #5568d3;
transform: translateX(-2px);
}
h1 {
color: #333;
margin: 0;
font-size: 2rem;
}
.section {
margin: 2rem 0;
padding: 1.5rem;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.help-btn {
padding: 0.5rem 1rem;
background: #fbbf24;
color: #78350f;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.help-btn:hover {
background: #f59e0b;
transform: scale(1.05);
}
h2 {
color: #495057;
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.status {
display: flex;
gap: 1rem;
padding: 1.5rem;
border-radius: 8px;
align-items: flex-start;
}
.status.success {
background: #d1fae5;
border-left: 4px solid #10b981;
}
.status.warning {
background: #fef3c7;
border-left: 4px solid #fbbf24;
}
.status-icon {
font-size: 2rem;
}
.status-content {
flex: 1;
}
.status-content p {
margin: 0.5rem 0;
}
.credentials-display {
font-family: 'Courier New', monospace;
color: #667eea;
font-weight: 600;
}
.token-info {
font-size: 0.9rem;
color: #666;
font-style: italic;
}
.button-group {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
flex-wrap: wrap;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
font-weight: 600;
color: #495057;
margin-bottom: 0.5rem;
}
.form-input {
width: 100%;
padding: 0.75rem;
border: 2px solid #dee2e6;
border-radius: 6px;
font-size: 1rem;
transition: all 0.3s ease;
}
.form-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.input-wrapper {
position: relative;
}
.toggle-password {
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
font-size: 1.2rem;
cursor: pointer;
padding: 0.25rem;
}
.help-text {
font-size: 0.875rem;
color: #6c757d;
margin-top: 0.5rem;
}
.btn {
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.error-message {
padding: 1rem;
background: #fee;
color: #c33;
border-radius: 6px;
border-left: 4px solid #c33;
margin: 1rem 0;
}
.success-message {
padding: 1rem;
background: #d1fae5;
color: #065f46;
border-radius: 6px;
border-left: 4px solid #10b981;
margin: 1rem 0;
}
.info-section {
background: linear-gradient(135deg, #e7f3ff 0%, #dbeafe 100%);
border: 2px solid #0284c7;
}
.info-content h3 {
color: #0c4a6e;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
}
.info-content ul {
margin: 0.5rem 0;
padding-left: 1.5rem;
}
.info-content li {
margin: 0.5rem 0;
line-height: 1.6;
}
/* Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.modal {
background: white;
border-radius: 12px;
padding: 2rem;
max-width: 500px;
width: 100%;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.modal-large {
max-width: 700px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.modal-header h3 {
margin: 0;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #999;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s ease;
}
.close-btn:hover {
background: #f0f0f0;
color: #333;
}
.modal h3 {
color: #333;
margin-bottom: 1rem;
}
.modal p {
color: #666;
line-height: 1.6;
margin-bottom: 1.5rem;
}
.modal-buttons {
display: flex;
gap: 1rem;
justify-content: flex-end;
}
.modal-body {
max-height: 60vh;
overflow-y: auto;
}
.guide-step {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid #e9ecef;
}
.guide-step:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.step-number {
flex-shrink: 0;
width: 40px;
height: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 1.2rem;
}
.step-content h4 {
margin: 0 0 0.5rem 0;
color: #333;
}
.step-content p {
margin: 0.5rem 0;
color: #666;
line-height: 1.6;
}
.step-content a {
color: #667eea;
text-decoration: none;
font-weight: 600;
border-bottom: 1px solid #667eea;
}
.step-content a:hover {
color: #5568d3;
border-bottom-color: #5568d3;
}
.step-content .warning {
background: #fef3c7;
padding: 0.75rem;
border-radius: 6px;
border-left: 3px solid #fbbf24;
margin-top: 0.5rem;
}
@media (max-width: 768px) {
h1 {
font-size: 1.5rem;
}
.button-group {
flex-direction: column;
}
.btn {
width: 100%;
}
}
</style>