Files
FragginWagon 161b758a1b Add support ticket documentation and relevant attachments
- 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.
2026-02-02 13:03:28 -05:00

1252 lines
31 KiB
Markdown
Raw Permalink 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.
# 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
```javascript
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
```vue
<!--
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:**
```javascript
// 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:**
```javascript
// 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:
```vue
<!-- 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:
```vue
<!-- 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):
```vue
<!-- 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
```css
.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
```vue
<!--
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:
1. Update router.js (simple, unblocks routes)
2. Update OAuthCallback.vue (enables OAuth callback)
3. Update DeveloperTools.vue (simple property update)
4. Update .env (add Discord credentials)
5. Create AuthenticationHub.vue (largest file)
6. Update ChallongeTest.vue (remove auth sections, add link)
7. Build and test