rapid interations of server side app and firefox extension
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row>
|
||||
<!-- Platform Cards -->
|
||||
<v-col
|
||||
v-for="(info, platform) in platforms"
|
||||
:key="platform"
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon :color="getPlatformColor(platform)" class="mr-2">
|
||||
{{ getPlatformIcon(platform) }}
|
||||
</v-icon>
|
||||
{{ info.name }}
|
||||
<v-spacer />
|
||||
<v-chip
|
||||
:color="getCredentialStatus(platform).color"
|
||||
size="small"
|
||||
>
|
||||
{{ getCredentialStatus(platform).text }}
|
||||
</v-chip>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<p class="text-body-2 mb-4">{{ info.description }}</p>
|
||||
|
||||
<div v-if="getCredential(platform)" class="mb-4">
|
||||
<v-list-item density="compact" class="px-0">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="success">mdi-check-circle</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Credentials stored</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
Last verified: {{ formatDate(getCredential(platform).last_verified) }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
v-if="getCredential(platform).expires_at"
|
||||
density="compact"
|
||||
class="px-0"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon :color="isExpiringSoon(platform) ? 'warning' : 'grey'">
|
||||
mdi-clock-outline
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>
|
||||
Expires: {{ formatDate(getCredential(platform).expires_at) }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</div>
|
||||
|
||||
<v-alert
|
||||
v-else
|
||||
type="info"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
class="mb-4"
|
||||
>
|
||||
No credentials stored for {{ info.name }}
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
v-if="getCredential(platform)"
|
||||
variant="text"
|
||||
color="primary"
|
||||
@click="verifyCredential(platform)"
|
||||
:loading="verifyingPlatform === platform"
|
||||
>
|
||||
Verify
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="primary"
|
||||
@click="openUploadDialog(platform, info)"
|
||||
>
|
||||
{{ getCredential(platform) ? 'Update' : 'Add' }} Credentials
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="getCredential(platform)"
|
||||
variant="text"
|
||||
color="error"
|
||||
@click="confirmDelete(platform)"
|
||||
>
|
||||
Remove
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Upload Dialog -->
|
||||
<v-dialog v-model="uploadDialog" max-width="600">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
{{ uploadPlatform?.name }} Credentials
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-alert type="info" variant="tonal" class="mb-4">
|
||||
<template v-if="uploadPlatform?.auth_type === 'token'">
|
||||
Paste your Discord user token below.
|
||||
<strong>Never share your token with anyone!</strong>
|
||||
</template>
|
||||
<template v-else>
|
||||
Paste your cookies in Netscape format, or use the Firefox extension
|
||||
for automatic export.
|
||||
</template>
|
||||
</v-alert>
|
||||
|
||||
<v-textarea
|
||||
v-model="credentialData"
|
||||
:label="uploadPlatform?.auth_type === 'token' ? 'Token' : 'Cookies (Netscape format)'"
|
||||
rows="8"
|
||||
variant="outlined"
|
||||
:placeholder="getPlaceholder()"
|
||||
/>
|
||||
|
||||
<v-expansion-panels class="mt-4">
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-title>
|
||||
How to get {{ uploadPlatform?.auth_type === 'token' ? 'your token' : 'cookies' }}
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<template v-if="uploadPlatform?.auth_type === 'token'">
|
||||
<ol>
|
||||
<li>Open Discord in your browser</li>
|
||||
<li>Press F12 to open Developer Tools</li>
|
||||
<li>Go to the Network tab</li>
|
||||
<li>Refresh the page</li>
|
||||
<li>Look for a request to discord.com/api</li>
|
||||
<li>Find the "Authorization" header in the request headers</li>
|
||||
<li>Copy the token value (without "Bearer " prefix)</li>
|
||||
</ol>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p><strong>Option 1: Firefox Extension (Recommended)</strong></p>
|
||||
<p>Install our Firefox extension to automatically export cookies.</p>
|
||||
<p class="mt-4"><strong>Option 2: Browser Export</strong></p>
|
||||
<ol>
|
||||
<li>Install a "cookies.txt" browser extension</li>
|
||||
<li>Visit {{ uploadPlatform?.name }} and log in</li>
|
||||
<li>Use the extension to export cookies</li>
|
||||
<li>Paste the exported text here</li>
|
||||
</ol>
|
||||
</template>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="uploadDialog = false">Cancel</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
:loading="uploading"
|
||||
:disabled="!credentialData"
|
||||
@click="uploadCredentials"
|
||||
>
|
||||
Save
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Delete Confirmation -->
|
||||
<v-dialog v-model="deleteDialog" max-width="400">
|
||||
<v-card>
|
||||
<v-card-title>Remove Credentials</v-card-title>
|
||||
<v-card-text>
|
||||
Are you sure you want to remove credentials for
|
||||
<strong>{{ platforms[deletePlatform]?.name }}</strong>?
|
||||
Downloads from this platform may fail without valid credentials.
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="deleteDialog = false">Cancel</v-btn>
|
||||
<v-btn color="error" @click="deleteCredential">Remove</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { credentialsApi, platformsApi } from '../services/api'
|
||||
import { useNotificationStore } from '../stores/notifications'
|
||||
|
||||
const notifications = useNotificationStore()
|
||||
|
||||
const platforms = ref({})
|
||||
const credentials = ref([])
|
||||
const loading = ref(false)
|
||||
const uploadDialog = ref(false)
|
||||
const uploadPlatform = ref(null)
|
||||
const uploadPlatformKey = ref('')
|
||||
const credentialData = ref('')
|
||||
const uploading = ref(false)
|
||||
const deleteDialog = ref(false)
|
||||
const deletePlatform = ref('')
|
||||
const verifyingPlatform = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
await loadData()
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [platformsRes, credentialsRes] = await Promise.all([
|
||||
platformsApi.list(),
|
||||
credentialsApi.list(),
|
||||
])
|
||||
platforms.value = platformsRes.data.platforms
|
||||
credentials.value = credentialsRes.data.items
|
||||
} catch (error) {
|
||||
notifications.error(`Failed to load: ${error.message}`)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function getPlatformIcon(platform) {
|
||||
const icons = {
|
||||
patreon: 'mdi-patreon',
|
||||
subscribestar: 'mdi-star',
|
||||
hentaifoundry: 'mdi-palette',
|
||||
discord: 'mdi-discord',
|
||||
}
|
||||
return icons[platform] || 'mdi-web'
|
||||
}
|
||||
|
||||
function getPlatformColor(platform) {
|
||||
const colors = {
|
||||
patreon: '#FF424D',
|
||||
subscribestar: '#FFD700',
|
||||
hentaifoundry: '#9C27B0',
|
||||
discord: '#5865F2',
|
||||
}
|
||||
return colors[platform] || 'grey'
|
||||
}
|
||||
|
||||
function getCredential(platform) {
|
||||
return credentials.value.find(c => c.platform === platform)
|
||||
}
|
||||
|
||||
function getCredentialStatus(platform) {
|
||||
const cred = getCredential(platform)
|
||||
if (!cred) return { color: 'grey', text: 'Not configured' }
|
||||
if (isExpiringSoon(platform)) return { color: 'warning', text: 'Expiring soon' }
|
||||
return { color: 'success', text: 'Active' }
|
||||
}
|
||||
|
||||
function isExpiringSoon(platform) {
|
||||
const cred = getCredential(platform)
|
||||
if (!cred?.expires_at) return false
|
||||
const expiresAt = new Date(cred.expires_at)
|
||||
const daysUntilExpiry = (expiresAt - new Date()) / (1000 * 60 * 60 * 24)
|
||||
return daysUntilExpiry < 7
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return 'Never'
|
||||
return new Date(dateStr).toLocaleString()
|
||||
}
|
||||
|
||||
function openUploadDialog(platform, info) {
|
||||
uploadPlatformKey.value = platform
|
||||
uploadPlatform.value = info
|
||||
credentialData.value = ''
|
||||
uploadDialog.value = true
|
||||
}
|
||||
|
||||
function getPlaceholder() {
|
||||
if (uploadPlatform.value?.auth_type === 'token') {
|
||||
return 'mfa.AbCdEf123456...'
|
||||
}
|
||||
return '# Netscape HTTP Cookie File\n.patreon.com\tTRUE\t/\tTRUE\t0\tsession_id\tabc123...'
|
||||
}
|
||||
|
||||
async function uploadCredentials() {
|
||||
uploading.value = true
|
||||
try {
|
||||
await credentialsApi.upload({
|
||||
platform: uploadPlatformKey.value,
|
||||
credential_type: uploadPlatform.value?.auth_type === 'token' ? 'token' : 'cookies',
|
||||
data: credentialData.value,
|
||||
})
|
||||
notifications.success('Credentials saved')
|
||||
uploadDialog.value = false
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
notifications.error(`Failed to save: ${error.response?.data?.error || error.message}`)
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyCredential(platform) {
|
||||
verifyingPlatform.value = platform
|
||||
try {
|
||||
const response = await credentialsApi.verify(platform)
|
||||
if (response.data.is_valid) {
|
||||
notifications.success(`${platforms.value[platform].name} credentials verified`)
|
||||
} else {
|
||||
notifications.warning(`Verification failed: ${response.data.error || 'Unknown error'}`)
|
||||
}
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
notifications.error(`Verification failed: ${error.message}`)
|
||||
} finally {
|
||||
verifyingPlatform.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete(platform) {
|
||||
deletePlatform.value = platform
|
||||
deleteDialog.value = true
|
||||
}
|
||||
|
||||
async function deleteCredential() {
|
||||
try {
|
||||
await credentialsApi.delete(deletePlatform.value)
|
||||
notifications.success('Credentials removed')
|
||||
deleteDialog.value = false
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
notifications.error(`Failed to remove: ${error.message}`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user