This repository has been archived on 2026-05-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
GallerySubscriber/frontend/src/views/Credentials.vue
T

363 lines
12 KiB
Vue

<template>
<div>
<v-row>
<!-- Platform Cards -->
<v-col
v-for="(info, platform) in platforms"
:key="platform"
cols="12"
md="6"
>
<v-card
:border="!!getCredential(platform)"
:style="!getCredential(platform)
? { border: '2px dashed var(--v-border-color)' }
: { borderColor: 'rgb(var(--v-theme-success))' }"
>
<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>
<div v-else class="text-center py-4 mb-4">
<v-icon size="40" color="secondary" :style="{ opacity: 0.5 }">mdi-key-remove</v-icon>
<div class="text-body-2 mt-2">No credentials configured</div>
<div class="text-caption text-medium-emphasis mt-1">Add credentials to enable downloads</div>
</div>
</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="getCredential(platform) ? 'text' : 'flat'"
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="uploadPlatformKey === 'pixiv'">
Paste your Pixiv OAuth refresh token below.
Run <code>gallery-dl oauth:pixiv</code> to obtain one.
</template>
<template v-else-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="uploadPlatformKey === 'pixiv'">
<ol>
<li>Install gallery-dl: <code>pip install gallery-dl</code></li>
<li>Run: <code>gallery-dl oauth:pixiv</code></li>
<li>A browser window will open for Pixiv login</li>
<li>After authorizing, copy the refresh token displayed</li>
<li>Paste the token here</li>
</ol>
<p class="mt-2 text-caption">The refresh token is a long string that gallery-dl uses to authenticate with Pixiv's API.</p>
</template>
<template v-else-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(() => {
// Don't block UI - load data in background
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',
pixiv: 'mdi-alpha-p-box',
deviantart: 'mdi-deviantart',
}
return icons[platform] || 'mdi-web'
}
function getPlatformColor(platform) {
const colors = {
patreon: '#FF424D',
subscribestar: '#FFD700',
hentaifoundry: '#9C27B0',
discord: '#5865F2',
pixiv: '#0096FA',
deviantart: '#05CC47',
}
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 (uploadPlatformKey.value === 'pixiv') {
return 'AZX1abc2DEF3ghi...'
}
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>