Add author auth, forking, tags, and stats tracking
Introduce token-based author authentication (register/verify API), skill forking with EditGate protection, tag metadata on skills, and download/push stats. Enhanced push scripts with token auth and per-skill filtering. Updated UI with stats, tags, and author info on skill cards.
This commit is contained in:
143
src/components/EditGate.vue
Normal file
143
src/components/EditGate.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<!-- Edit button -->
|
||||
<button
|
||||
@click="handleClick"
|
||||
class="inline-flex items-center gap-1.5 rounded-lg border border-white/[0.08] bg-surface-200 px-3.5 py-2 text-sm font-medium text-gray-300 hover:border-white/[0.15] hover:text-white transition-all"
|
||||
>
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
|
||||
<!-- Modal backdrop -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showModal = false">
|
||||
<div class="w-full max-w-md rounded-2xl border border-white/[0.08] bg-[var(--color-surface-200)] p-6 shadow-2xl">
|
||||
<h3 class="text-lg font-semibold text-white mb-1">Author Verification</h3>
|
||||
<p class="text-sm text-gray-500 mb-4">
|
||||
This skill is owned by <strong class="text-gray-300">{{ authorName || authorEmail }}</strong>. Enter your token to edit.
|
||||
</p>
|
||||
|
||||
<form @submit.prevent="verify">
|
||||
<input
|
||||
ref="tokenInput"
|
||||
v-model="token"
|
||||
type="password"
|
||||
placeholder="Paste your author token..."
|
||||
class="w-full rounded-xl border border-white/[0.06] bg-[var(--color-surface-100)] px-4 py-2.5 text-sm text-white placeholder-gray-600 font-mono focus:border-[var(--color-accent-500)]/50 focus:outline-none focus:ring-1 focus:ring-[var(--color-accent-500)]/20 transition-all"
|
||||
/>
|
||||
<p v-if="error" class="mt-2 text-sm text-red-400">{{ error }}</p>
|
||||
|
||||
<div class="mt-4 flex items-center gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="verifying || !token"
|
||||
class="inline-flex items-center gap-2 rounded-xl bg-[var(--color-accent-500)] px-5 py-2 text-sm font-semibold text-white shadow-lg shadow-[var(--color-accent-500)]/20 hover:bg-[var(--color-accent-600)] disabled:opacity-50 active:scale-[0.97] transition-all"
|
||||
>
|
||||
<svg v-if="verifying" class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
{{ verifying ? 'Verifying...' : 'Continue to Edit' }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@click="forkSkill"
|
||||
class="text-sm text-[var(--color-accent-400)] hover:text-[var(--color-accent-300)] transition-colors"
|
||||
>
|
||||
Fork instead
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@click="showModal = false"
|
||||
class="ml-auto text-sm text-gray-600 hover:text-gray-300 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
slug: string;
|
||||
authorEmail?: string;
|
||||
authorName?: string;
|
||||
authorHasToken?: boolean;
|
||||
}>();
|
||||
|
||||
const showModal = ref(false);
|
||||
const token = ref('');
|
||||
const error = ref('');
|
||||
const verifying = ref(false);
|
||||
const tokenInput = ref<HTMLInputElement>();
|
||||
|
||||
async function handleClick() {
|
||||
if (!props.authorEmail || !props.authorHasToken) {
|
||||
window.location.href = `/${props.slug}/edit`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Try saved token first
|
||||
const saved = localStorage.getItem('skillshere-token') || '';
|
||||
if (saved) {
|
||||
try {
|
||||
const res = await fetch('/api/auth/verify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: props.authorEmail, token: saved }),
|
||||
});
|
||||
if (res.ok) {
|
||||
localStorage.setItem('skillshere-token', saved);
|
||||
window.location.href = `/${props.slug}/edit`;
|
||||
return;
|
||||
}
|
||||
} catch { /* fall through to modal */ }
|
||||
}
|
||||
|
||||
showModal.value = true;
|
||||
error.value = '';
|
||||
token.value = '';
|
||||
nextTick(() => tokenInput.value?.focus());
|
||||
}
|
||||
|
||||
async function verify() {
|
||||
verifying.value = true;
|
||||
error.value = '';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/auth/verify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: props.authorEmail, token: token.value }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
error.value = data.error || 'Invalid token';
|
||||
return;
|
||||
}
|
||||
|
||||
// Store token for the editor to use
|
||||
localStorage.setItem('skillshere-token', token.value);
|
||||
window.location.href = `/${props.slug}/edit`;
|
||||
} catch {
|
||||
error.value = 'Could not verify token';
|
||||
} finally {
|
||||
verifying.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function forkSkill() {
|
||||
showModal.value = false;
|
||||
window.location.href = `/new?from=${encodeURIComponent(props.slug)}`;
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user