- Created new markdown file for Support Ticket - 3224942 with a link to the support page. - Added a separate markdown file for Supprt Tickets with the same link. - Updated workspace files to include new markdown files and attachments. - Added various attachments related to the support ticket, including images and PDFs.
31 KiB
AUTH HUB REFACTOR - COMPLETE CODE READY FOR APPLICATION
This file contains ALL code needed to complete the remaining Phase 1 and Phase 2 updates. Simply copy each section and apply to the corresponding file when file editors are available.
FILE 1: src/router/index.js
Action: Replace entire file
import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue';
import GamemasterManager from '../views/GamemasterManager.vue';
import GamemasterExplorer from '../views/GamemasterExplorer.vue';
import ChallongeTest from '../views/ChallongeTest.vue';
import AuthenticationHub from '../views/AuthenticationHub.vue';
import ClientCredentialsManager from '../views/ClientCredentialsManager.vue';
import OAuthCallback from '../views/OAuthCallback.vue';
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/gamemaster',
name: 'GamemasterManager',
component: GamemasterManager
},
{
path: '/gamemaster-explorer',
name: 'GamemasterExplorer',
component: GamemasterExplorer
},
{
path: '/challonge-test',
name: 'ChallongeTest',
component: ChallongeTest
},
{
path: '/auth',
name: 'AuthenticationHub',
component: AuthenticationHub
},
{
path: '/client-credentials',
name: 'ClientCredentialsManager',
component: ClientCredentialsManager
},
{
path: '/oauth/callback',
name: 'OAuthCallback',
component: OAuthCallback
},
// Legacy redirects for backwards compatibility
{
path: '/api-key-manager',
redirect: '/auth'
},
{
path: '/settings',
redirect: '/auth'
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;
FILE 2: src/views/OAuthCallback.vue
Action: Replace entire file
<!--
OAuth Callback Handler
Handles OAuth callbacks from any provider (Challonge, Discord, etc.)
Exchanges authorization code for access tokens
Supports return_to query parameter for post-auth redirect
-->
<template>
<div class="oauth-callback">
<div class="container">
<div class="callback-card">
<div v-if="loading" class="loading-state">
<div class="spinner"></div>
<h2>Authenticating...</h2>
<p>Please wait while we complete your {{ provider }} OAuth login</p>
</div>
<div v-else-if="error" class="error-state">
<div class="error-icon">✕</div>
<h2>Authentication Failed</h2>
<p class="error-message">{{ error }}</p>
<router-link to="/auth" class="btn btn-primary">
Back to Authentication Settings
</router-link>
</div>
<div v-else-if="success" class="success-state">
<div class="success-icon">✓</div>
<h2>{{ provider }} Authentication Successful!</h2>
<p>You're now authenticated with {{ provider }}</p>
<p class="redirect-info">Redirecting in {{ countdown }} seconds...</p>
<router-link :to="returnTo || '/auth'" class="btn btn-primary">
Continue to Authentication Settings
</router-link>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useOAuth } from '../composables/useOAuth.js';
const route = useRoute();
const router = useRouter();
const loading = ref(true);
const error = ref(null);
const success = ref(false);
const countdown = ref(3);
const provider = ref('challonge');
const returnTo = ref(null);
onMounted(async () => {
// Get provider from query or sessionStorage (default: 'challonge' for backwards compatibility)
provider.value = route.query.provider || sessionStorage.getItem('oauth_provider') || 'challonge';
// Get redirect destination (default: /auth)
returnTo.value = route.query.return_to || sessionStorage.getItem('oauth_return_to') || '/auth';
// Get OAuth parameters from URL
const code = route.query.code;
const state = route.query.state;
const errorParam = route.query.error;
const errorDescription = route.query.error_description;
// Handle OAuth errors from provider
if (errorParam) {
loading.value = false;
error.value = errorDescription || `OAuth error: ${errorParam}`;
console.warn(`OAuth error from ${provider.value}:`, errorParam);
return;
}
// Validate required parameters
if (!code || !state) {
loading.value = false;
error.value = 'Missing authorization code or state parameter';
return;
}
try {
// Exchange code for tokens using unified OAuth handler
const oauth = useOAuth(provider.value);
await oauth.exchangeCode(code, state);
loading.value = false;
success.value = true;
// Start countdown redirect
const interval = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(interval);
router.push(returnTo.value);
}
}, 1000);
} catch (err) {
loading.value = false;
error.value = err.message || 'Failed to complete OAuth authentication';
console.error('OAuth callback error:', err);
}
});
</script>
<style scoped>
.oauth-callback {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 2rem;
}
.container {
max-width: 400px;
width: 100%;
}
.callback-card {
background: white;
border-radius: 8px;
padding: 2rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
}
.loading-state,
.error-state,
.success-state {
text-align: center;
}
.spinner {
width: 48px;
height: 48px;
border: 4px solid #667eea;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 1rem;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.error-icon {
font-size: 3rem;
color: #dc3545;
margin-bottom: 1rem;
}
.success-icon {
font-size: 3rem;
color: #28a745;
margin-bottom: 1rem;
}
h2 {
margin: 1rem 0;
font-size: 1.5rem;
color: #333;
}
p {
color: #666;
margin: 0.5rem 0;
}
.error-message {
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 4px;
padding: 1rem;
margin: 1rem 0;
color: #856404;
}
.redirect-info {
font-size: 0.9rem;
font-weight: 500;
color: #666;
}
.btn {
display: inline-block;
margin-top: 1.5rem;
padding: 0.75rem 1.5rem;
border-radius: 4px;
text-decoration: none;
font-weight: 500;
transition: all 0.2s;
border: none;
cursor: pointer;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
</style>
FILE 3: src/components/DeveloperTools.vue
Action: Find and replace the isAvailable computed property (around line 146)
Old Code:
// Only show in development mode
const isAvailable = computed(() => {
const isDev = process.env.NODE_ENV === 'development';
const isAuthenticatedInProduction = process.env.NODE_ENV === 'production' && user.value;
return isDev || isAuthenticatedInProduction;
});
New Code:
// Show if user has developer_tools.view permission (or authenticated in dev mode)
const isAvailable = computed(() => {
// Must be authenticated
if (!user.value) return false;
// Check for explicit permission (most secure in production)
if (user.value.permissions?.includes('developer_tools.view')) {
return true;
}
// In development, show for any authenticated user
if (process.env.NODE_ENV === 'development') {
return true;
}
return false;
});
FILE 4: .env (server/.env)
Action: Add these lines at the end of the file
# Discord OAuth Configuration
VITE_DISCORD_CLIENT_ID=your_discord_app_id_here
VITE_DISCORD_REDIRECT_URI=http://localhost:5173/oauth/callback
Note: Before using Discord OAuth, register an application at https://discord.com/developers/applications and replace your_discord_app_id_here with your actual Client ID.
FILE 5: src/views/ChallongeTest.vue
Action 1: Find and remove the OAuth Authentication section (lines ~49-120)
Look for this section:
<!-- OAuth Authentication (v2.1 only) -->
<div
v-if="apiVersion === 'v2.1'"
class="control-group oauth-section collapsible-group"
>
...entire section...
</div>
Remove the entire <!-- OAuth Authentication --> section.
Action 2: Find and remove the API Key Configuration section (lines ~28-45)
Look for:
<!-- API Key Configuration -->
<div class="control-group collapsible-group">
...entire section...
</div>
Remove the entire API Key Configuration section.
Action 3: Find and remove the Client Credentials section
Look for the client credentials management section and remove it.
Action 4: Replace the removed sections with this info banner
Add this BEFORE the API Version Selector (where the sections were removed):
<!-- Authentication Settings Link -->
<div class="control-group info-section">
<div class="info-message">
<h4>⚙️ Configure Your Authentication</h4>
<p>Manage your Challonge API keys, OAuth tokens, and other authentication methods in the <strong>Authentication Settings</strong>.</p>
<router-link to="/auth" class="btn btn-secondary">
Go to Authentication Settings
</router-link>
</div>
</div>
Action 5: Add these styles to the <style scoped> section
.info-section {
background: #e3f2fd;
border-left: 4px solid #2196f3;
padding: 1.5rem;
border-radius: 4px;
margin-bottom: 1.5rem;
}
.info-section h4 {
margin-top: 0;
color: #1976d2;
font-size: 1.1rem;
}
.info-section p {
margin: 0.5rem 0 1rem;
color: #555;
line-height: 1.5;
}
.btn-secondary {
background: #2196f3;
color: white;
padding: 0.5rem 1rem;
border-radius: 4px;
text-decoration: none;
display: inline-block;
font-weight: 500;
transition: all 0.2s;
border: none;
cursor: pointer;
}
.btn-secondary:hover {
background: #1976d2;
transform: translateY(-2px);
}
FILE 6: src/views/AuthenticationHub.vue
Action: Create new file with complete content below
<!--
Authentication Hub
Unified interface for managing all authentication methods across platforms
Supports: Challonge (API Key, OAuth, Client Credentials), Discord (OAuth)
Features:
- Tab-based interface for each platform
- Token status and expiry display
- Manual refresh buttons
- Auto-refresh info
- Success/error notifications
-->
<template>
<div class="auth-hub">
<div class="container">
<div class="header">
<h1>🔐 Authentication Settings</h1>
<p class="subtitle">Manage your authentication credentials and tokens across all platforms</p>
</div>
<!-- Tabs Navigation -->
<div class="tabs-nav">
<button
v-for="platform in platforms"
:key="platform.name"
:class="['tab-button', { active: activePlatform === platform.name }]"
@click="activePlatform = platform.name"
>
{{ platform.icon }} {{ platform.label }}
</button>
</div>
<!-- Notifications -->
<div v-if="successMessage" class="notification success">
<span>✓ {{ successMessage }}</span>
<button @click="successMessage = ''" class="close-btn">×</button>
</div>
<div v-if="errorMessage" class="notification error">
<span>✕ {{ errorMessage }}</span>
<button @click="errorMessage = ''" class="close-btn">×</button>
</div>
<!-- Tab Content -->
<div class="tab-content">
<!-- Challonge Tab -->
<div v-if="activePlatform === 'challonge'" class="platform-section">
<h2>🏆 Challonge Authentication</h2>
<p class="platform-description">
Configure your Challonge API access using API keys, OAuth tokens, or client credentials
</p>
<!-- API Key Section -->
<div class="auth-method">
<div class="method-header">
<h3>API Key</h3>
<span :class="['status', { active: hasChallongeApiKey }]">
{{ hasChallongeApiKey ? '✓ Connected' : '○ Not Connected' }}
</span>
</div>
<p class="method-description">Direct API key authentication for v1 and v2.1 APIs</p>
<div class="form-group">
<input
v-model="challongeApiKey"
type="password"
placeholder="Enter your Challonge API key"
@keyup.enter="saveChallongeApiKey"
/>
<button @click="saveChallongeApiKey" class="btn btn-primary">
{{ hasChallongeApiKey ? 'Update' : 'Save' }} API Key
</button>
<button v-if="hasChallongeApiKey" @click="deleteChallongeApiKey" class="btn btn-danger">
Delete
</button>
</div>
<p class="help-text">
Get your API key from
<a href="https://challonge.com/settings/developer" target="_blank">Challonge Developer Settings</a>
</p>
</div>
<!-- OAuth Section -->
<div class="auth-method">
<div class="method-header">
<h3>OAuth 2.0</h3>
<span :class="['status', { active: isChallongeOAuthAuthenticated }]">
{{ isChallongeOAuthAuthenticated ? '✓ Connected' : '○ Not Connected' }}
</span>
</div>
<p class="method-description">User token authentication for v2.1 API (APPLICATION scope)</p>
<div v-if="isChallongeOAuthAuthenticated" class="token-info">
<div class="token-detail">
<span class="label">Status:</span>
<span class="value">✅ Authenticated</span>
</div>
<div class="token-detail">
<span class="label">Expires in:</span>
<span class="value">{{ formatExpiryTime(challongeOAuthExpiresIn) }}</span>
</div>
<div v-if="challongeOAuthRefreshedAt" class="token-detail">
<span class="label">Last refreshed:</span>
<span class="value">{{ formatDate(challongeOAuthRefreshedAt) }}</span>
</div>
<div class="button-group">
<button @click="refreshChallongeOAuth" :disabled="oauthLoading" class="btn btn-secondary">
{{ oauthLoading ? '⏳ Refreshing...' : '🔄 Refresh Token' }}
</button>
<button @click="disconnectChallongeOAuth" class="btn btn-danger">
Disconnect
</button>
</div>
</div>
<div v-else class="button-group">
<button @click="connectChallongeOAuth" :disabled="oauthLoading" class="btn btn-primary">
{{ oauthLoading ? '⏳ Connecting...' : '🔗 Connect with Challonge OAuth' }}
</button>
</div>
<p class="help-text">
Register your application at
<a href="https://connect.challonge.com" target="_blank">Challonge OAuth</a>
and use it for APPLICATION scope access
</p>
</div>
<!-- Client Credentials Section -->
<div class="auth-method">
<div class="method-header">
<h3>Client Credentials</h3>
<span :class="['status', { active: hasChallongeClientCredentials }]">
{{ hasChallongeClientCredentials ? '✓ Connected' : '○ Not Connected' }}
</span>
</div>
<p class="method-description">For APPLICATION scope access with client ID and secret</p>
<div v-if="hasChallongeClientCredentials" class="token-info">
<div class="token-detail">
<span class="label">Client ID:</span>
<code>{{ challongeClientId?.substring(0, 10) }}...</code>
</div>
<div class="token-detail">
<span class="label">Status:</span>
<span class="value">{{ isChallongeClientCredentialsValid ? '✅ Valid' : '⚠️ Expired' }}</span>
</div>
<div v-if="challongeClientExpiresIn" class="token-detail">
<span class="label">Token expires in:</span>
<span class="value">{{ formatExpiryTime(challongeClientExpiresIn) }}</span>
</div>
<div class="button-group">
<button @click="deleteChallongeClientCredentials" class="btn btn-danger">
Delete
</button>
</div>
</div>
<div v-else>
<div class="form-group">
<input
v-model="newClientId"
type="text"
placeholder="Client ID"
@keyup.enter="saveChallongeClientCredentials"
/>
<input
v-model="newClientSecret"
type="password"
placeholder="Client Secret"
@keyup.enter="saveChallongeClientCredentials"
/>
<button @click="saveChallongeClientCredentials" class="btn btn-primary">
Save Client Credentials
</button>
</div>
</div>
<p class="help-text">
Get credentials from
<a href="https://challonge.com/settings/developer" target="_blank">Challonge Developer Settings</a>
</p>
</div>
</div>
<!-- Discord Tab -->
<div v-if="activePlatform === 'discord'" class="platform-section">
<h2>🎮 Discord Authentication</h2>
<p class="platform-description">
Verify your Discord identity for access control and developer features
</p>
<div class="auth-method">
<div class="method-header">
<h3>Discord OAuth</h3>
<span :class="['status', { active: isDiscordAuthenticated }]">
{{ isDiscordAuthenticated ? '✓ Connected' : '○ Not Connected' }}
</span>
</div>
<p class="method-description">Secure identity verification using Discord account</p>
<div v-if="isDiscordAuthenticated" class="token-info">
<div class="token-detail">
<span class="label">Username:</span>
<span class="value">{{ discordUsername || 'Loading...' }}</span>
</div>
<div class="token-detail">
<span class="label">Status:</span>
<span class="value">✅ Authenticated</span>
</div>
<div v-if="discordExpiresIn" class="token-detail">
<span class="label">Expires in:</span>
<span class="value">{{ formatExpiryTime(discordExpiresIn) }}</span>
</div>
<div class="button-group">
<button @click="refreshDiscordAuth" :disabled="discordLoading" class="btn btn-secondary">
{{ discordLoading ? '⏳ Refreshing...' : '🔄 Refresh' }}
</button>
<button @click="disconnectDiscord" class="btn btn-danger">
Disconnect
</button>
</div>
</div>
<div v-else class="button-group">
<button @click="connectDiscord" :disabled="discordLoading" class="btn btn-primary">
{{ discordLoading ? '⏳ Connecting...' : '🔗 Connect with Discord' }}
</button>
</div>
<p class="help-text">
Create Discord application at
<a href="https://discord.com/developers/applications" target="_blank">Discord Developer Portal</a>
</p>
</div>
</div>
</div>
<!-- Footer -->
<div class="footer">
<p>Your authentication tokens are stored securely in your browser's local storage.</p>
<router-link to="/" class="btn-link">← Back Home</router-link>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useChallongeApiKey } from '../composables/useChallongeApiKey.js';
import { useChallongeOAuth } from '../composables/useChallongeOAuth.js';
import { useChallongeClientCredentials } from '../composables/useChallongeClientCredentials.js';
import { useDiscordOAuth } from '../composables/useDiscordOAuth.js';
import { getAllPlatforms } from '../config/platforms.js';
// State
const activePlatform = ref('challonge');
const successMessage = ref('');
const errorMessage = ref('');
const oauthLoading = ref(false);
const discordLoading = ref(false);
// Challonge API Key
const { apiKey: challongeApiKey, save: saveApiKey, delete: deleteApiKey } = useChallongeApiKey();
const newChallongeApiKey = ref('');
const hasChallongeApiKey = computed(() => !!challongeApiKey.value);
// Challonge OAuth
const challongeOAuth = useChallongeOAuth();
const isChallongeOAuthAuthenticated = computed(() => challongeOAuth.isAuthenticated.value);
const challongeOAuthExpiresIn = computed(() => challongeOAuth.expiresIn.value);
const challongeOAuthRefreshedAt = computed(() => {
return challongeOAuth.tokens.value?.refreshed_at || challongeOAuth.tokens.value?.created_at;
});
// Challonge Client Credentials
const challengeClientCreds = useChallongeClientCredentials();
const hasChallongeClientCredentials = computed(() => challengeClientCreds.isConfigured.value);
const isChallongeClientCredentialsValid = computed(() => challengeClientCreds.isValid.value);
const challongeClientId = computed(() => challengeClientCreds.clientId.value);
const challongeClientExpiresIn = computed(() => challengeClientCreds.expiresIn.value);
const newClientId = ref('');
const newClientSecret = ref('');
// Discord OAuth
const discord = useDiscordOAuth();
const isDiscordAuthenticated = computed(() => discord.hasDiscordAuth.value);
const discordUsername = computed(() => discord.discordUsername.value);
const discordExpiresIn = computed(() => discord.expiresIn.value);
// Get all platforms for tab navigation
const platforms = computed(() => getAllPlatforms());
// Methods
function saveChallongeApiKey() {
try {
saveApiKey(newChallongeApiKey.value);
newChallongeApiKey.value = '';
successMessage.value = 'Challonge API key saved successfully!';
setTimeout(() => (successMessage.value = ''), 3000);
} catch (err) {
errorMessage.value = err.message;
}
}
function deleteChallongeApiKey() {
if (confirm('Are you sure? This will remove your API key.')) {
deleteApiKey();
successMessage.value = 'Challonge API key deleted';
setTimeout(() => (successMessage.value = ''), 3000);
}
}
async function connectChallongeOAuth() {
try {
oauthLoading.value = true;
challongeOAuth.login({ return_to: '/auth' });
} catch (err) {
errorMessage.value = err.message;
oauthLoading.value = false;
}
}
async function refreshChallongeOAuth() {
try {
oauthLoading.value = true;
await challongeOAuth.refreshToken();
successMessage.value = 'Challonge OAuth token refreshed!';
setTimeout(() => (successMessage.value = ''), 3000);
} catch (err) {
errorMessage.value = err.message;
} finally {
oauthLoading.value = false;
}
}
function disconnectChallongeOAuth() {
if (confirm('Disconnect Challonge OAuth? You will need to reconnect to use OAuth features.')) {
challongeOAuth.logout();
successMessage.value = 'Disconnected from Challonge OAuth';
setTimeout(() => (successMessage.value = ''), 3000);
}
}
function saveChallongeClientCredentials() {
try {
challengeClientCreds.save(newClientId.value, newClientSecret.value);
newClientId.value = '';
newClientSecret.value = '';
successMessage.value = 'Client credentials saved!';
setTimeout(() => (successMessage.value = ''), 3000);
} catch (err) {
errorMessage.value = err.message;
}
}
function deleteChallongeClientCredentials() {
if (confirm('Delete client credentials?')) {
challengeClientCreds.delete();
successMessage.value = 'Client credentials deleted';
setTimeout(() => (successMessage.value = ''), 3000);
}
}
async function connectDiscord() {
try {
discordLoading.value = true;
discord.login({ return_to: '/auth' });
} catch (err) {
errorMessage.value = err.message;
discordLoading.value = false;
}
}
async function refreshDiscordAuth() {
try {
discordLoading.value = true;
await discord.refreshToken();
successMessage.value = 'Discord token refreshed!';
setTimeout(() => (successMessage.value = ''), 3000);
} catch (err) {
errorMessage.value = err.message;
} finally {
discordLoading.value = false;
}
}
function disconnectDiscord() {
if (confirm('Disconnect Discord? You will need to reconnect for Discord features.')) {
discord.logout();
successMessage.value = 'Disconnected from Discord';
setTimeout(() => (successMessage.value = ''), 3000);
}
}
function formatExpiryTime(seconds) {
if (!seconds) return 'Unknown';
if (seconds < 0) return 'Expired';
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) return `${hours}h ${minutes}m`;
return `${minutes}m`;
}
function formatDate(timestamp) {
if (!timestamp) return 'Never';
return new Date(timestamp).toLocaleString();
}
// Load Discord profile on mount if authenticated
onMounted(async () => {
if (isDiscordAuthenticated.value) {
try {
await discord.fetchUserProfile();
} catch (err) {
console.error('Failed to load Discord profile:', err);
}
}
});
</script>
<style scoped>
.auth-hub {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 2rem 0;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 0 1rem;
}
.header {
text-align: center;
color: white;
margin-bottom: 2rem;
}
.header h1 {
font-size: 2.5rem;
margin: 0 0 0.5rem;
font-weight: 700;
}
.subtitle {
font-size: 1.1rem;
opacity: 0.95;
margin: 0;
}
/* Tabs Navigation */
.tabs-nav {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
}
.tab-button {
padding: 1rem 1.5rem;
border: none;
background: transparent;
color: white;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
border-bottom: 3px solid transparent;
transition: all 0.2s;
opacity: 0.8;
}
.tab-button:hover {
opacity: 1;
}
.tab-button.active {
opacity: 1;
border-bottom-color: white;
}
/* Notifications */
.notification {
padding: 1rem;
border-radius: 6px;
margin-bottom: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 500;
}
.notification.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.notification.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.close-btn {
background: transparent;
border: none;
font-size: 1.5rem;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s;
}
.close-btn:hover {
opacity: 1;
}
/* Tab Content */
.tab-content {
background: white;
border-radius: 8px;
padding: 2rem;
margin-bottom: 2rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}
.platform-section h2 {
margin-top: 0;
color: #333;
margin-bottom: 0.5rem;
}
.platform-description {
color: #666;
margin-bottom: 2rem;
line-height: 1.6;
}
/* Auth Method */
.auth-method {
margin-bottom: 2.5rem;
padding-bottom: 2rem;
border-bottom: 1px solid #eee;
}
.auth-method:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.method-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.method-header h3 {
margin: 0;
color: #333;
font-size: 1.1rem;
}
.status {
font-size: 0.9rem;
padding: 0.25rem 0.75rem;
border-radius: 20px;
background: #f0f0f0;
color: #666;
}
.status.active {
background: #d4edda;
color: #155724;
font-weight: 500;
}
.method-description {
color: #666;
margin: 0.5rem 0 1rem;
font-size: 0.95rem;
}
/* Token Info */
.token-info {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 1.5rem;
margin-bottom: 1rem;
}
.token-detail {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
font-size: 0.95rem;
}
.token-detail:last-child {
margin-bottom: 0;
}
.token-detail .label {
color: #666;
font-weight: 500;
}
.token-detail .value {
color: #333;
font-weight: 600;
}
.token-detail code {
background: #fff;
padding: 0.25rem 0.5rem;
border-radius: 3px;
border: 1px solid #dee2e6;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.85rem;
}
/* Forms */
.form-group {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1rem;
}
.form-group input {
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.95rem;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
/* Buttons */
.button-group {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
font-size: 0.95rem;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
transform: translateY(-2px);
}
.btn-secondary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
transform: translateY(-2px);
}
.help-text {
font-size: 0.85rem;
color: #666;
margin: 1rem 0 0;
}
.help-text a {
color: #667eea;
text-decoration: none;
}
.help-text a:hover {
text-decoration: underline;
}
/* Footer */
.footer {
background: white;
border-radius: 8px;
padding: 2rem;
text-align: center;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}
.footer p {
color: #666;
margin: 0 0 1rem;
}
.btn-link {
color: #667eea;
text-decoration: none;
font-weight: 500;
transition: color 0.2s;
}
.btn-link:hover {
color: #5568d3;
text-decoration: underline;
}
/* Responsive */
@media (max-width: 768px) {
.header h1 {
font-size: 1.75rem;
}
.tabs-nav {
gap: 0.5rem;
}
.tab-button {
padding: 0.75rem 1rem;
font-size: 0.9rem;
}
.method-header {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.token-detail {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
}
.form-group {
flex-direction: column;
}
.button-group {
flex-direction: column;
}
.btn {
width: 100%;
text-align: center;
}
}
</style>
Summary
All code is ready to apply. The order is:
- Update router.js (simple, unblocks routes)
- Update OAuthCallback.vue (enables OAuth callback)
- Update DeveloperTools.vue (simple property update)
- Update .env (add Discord credentials)
- Create AuthenticationHub.vue (largest file)
- Update ChallongeTest.vue (remove auth sections, add link)
- Build and test