feat: Implement comprehensive security enhancements and deployment guide for AmayoWeb
- Added a detailed deployment guide (DEPLOYMENT_GUIDE.md) for frontend and backend setup. - Created an index documentation (INDEX.md) summarizing changes and available resources. - Established Nginx security configuration (NGINX_SECURITY_CONFIG.md) to protect backend IP and enforce rate limiting. - Developed a backend security guide (SECURITY_BACKEND_GUIDE.md) outlining security measures and best practices. - Introduced middleware for security, including rate limiting, CORS, and Cloudflare validation. - Updated frontend components and services to improve security and user experience. - Implemented logging and monitoring strategies for better security oversight.
This commit is contained in:
16
AmayoWeb/public/.well-known/api-config.json
Normal file
16
AmayoWeb/public/.well-known/api-config.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"endpoint": "https://api.amayo.dev/api",
|
||||
"version": "1.0.0",
|
||||
"features": {
|
||||
"rateLimit": true,
|
||||
"cors": true,
|
||||
"csrf": true
|
||||
},
|
||||
"security": {
|
||||
"requiresToken": true,
|
||||
"allowedOrigins": [
|
||||
"https://docs.amayo.dev",
|
||||
"https://amayo.dev"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,11 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<AnimatedBackground />
|
||||
<IslandNavbar />
|
||||
<HeroSection />
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import AnimatedBackground from './components/AnimatedBackground.vue'
|
||||
import IslandNavbar from './components/IslandNavbar.vue'
|
||||
import HeroSection from './components/HeroSection.vue'
|
||||
import { useTheme } from './composables/useTheme'
|
||||
|
||||
const { initTheme } = useTheme()
|
||||
@@ -35,18 +29,28 @@ onMounted(() => {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #0a0a0a;
|
||||
color: white;
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#app {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
max-width: 100vw;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
@@ -26,10 +26,4 @@ a,
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +59,8 @@ import { botService } from '@/services/bot'
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const texts = {
|
||||
es: 'Un bot con mucha personalidad',
|
||||
en: 'A bot beyond comparison'
|
||||
es: 'El Mejor Bot de Discord',
|
||||
en: 'The Best Discord Bot'
|
||||
}
|
||||
|
||||
const displayText = ref('')
|
||||
@@ -202,7 +202,7 @@ const inviteBot = () => {
|
||||
}
|
||||
|
||||
.hero-title::before {
|
||||
content: 'Un bot con mucha personalidad';
|
||||
content: 'El Mejor Bot de Discord';
|
||||
font-size: 4rem;
|
||||
font-weight: 800;
|
||||
visibility: hidden;
|
||||
@@ -349,19 +349,19 @@ const inviteBot = () => {
|
||||
|
||||
.card-1 {
|
||||
top: 30px;
|
||||
right: -538px;
|
||||
right: 405px;
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.card-2 {
|
||||
top: 190px;
|
||||
right: -772px;
|
||||
right: 185px;
|
||||
animation: float 6s ease-in-out infinite 2s;
|
||||
}
|
||||
|
||||
.card-3 {
|
||||
bottom: 50px;
|
||||
right: -540px;
|
||||
bottom: -2px;
|
||||
right: -32px;;
|
||||
animation: float 6s ease-in-out infinite 4s;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</button>
|
||||
|
||||
<!-- Navigation Buttons -->
|
||||
<a href="#get-started" class="nav-btn primary">
|
||||
<a href="/docs" class="nav-btn primary">
|
||||
{{ t('navbar.getStarted') }}
|
||||
</a>
|
||||
<a href="/dashboard" class="nav-btn secondary">
|
||||
@@ -126,7 +126,7 @@ onUnmounted(() => {
|
||||
.island-navbar {
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
left: 98%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1000;
|
||||
width: 150%;
|
||||
|
||||
213
AmayoWeb/src/components/docs/HeroSection.vue
Normal file
213
AmayoWeb/src/components/docs/HeroSection.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<section class="hero-section">
|
||||
<div class="hero-content">
|
||||
<div class="hero-text">
|
||||
<h1 class="hero-title">
|
||||
<span class="title-text">{{ titleText }}</span>
|
||||
</h1>
|
||||
<p class="hero-subtitle">{{ t('hero_docs.subtitle') }}</p>
|
||||
|
||||
<div class="hero-actions">
|
||||
<button class="hero-btn primary" @click="scrollToFeatures">
|
||||
{{ t('hero_docs.exploreFeatures') }}
|
||||
</button>
|
||||
<button class="hero-btn secondary" @click="inviteBot">
|
||||
{{ t('hero_docs.inviteBot') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { botService } from '@/services/bot'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const titleText = computed(() => {
|
||||
return locale.value === 'es'
|
||||
? 'Comandos, Tickets y Moderación'
|
||||
: 'Commands, Tickets, and Moderation'
|
||||
})
|
||||
|
||||
const isLoading = ref(true)
|
||||
|
||||
const stats = ref({
|
||||
servers: '...',
|
||||
users: '...',
|
||||
commands: '...'
|
||||
})
|
||||
|
||||
// Cargar estadísticas reales del bot
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const data = await botService.getStats()
|
||||
stats.value = {
|
||||
servers: botService.formatNumber(data.servers || 0),
|
||||
users: botService.formatNumber(data.users || 0),
|
||||
commands: botService.formatNumber(data.commands || 0)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading stats:', error)
|
||||
// Valores por defecto si falla
|
||||
stats.value = {
|
||||
servers: '0',
|
||||
users: '0',
|
||||
commands: '0'
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStats()
|
||||
// Actualizar estadísticas cada 5 minutos
|
||||
setInterval(loadStats, 5 * 60 * 1000)
|
||||
})
|
||||
|
||||
const scrollToFeatures = () => {
|
||||
document.querySelector('#features')?.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
|
||||
const inviteBot = () => {
|
||||
window.open('https://discord.com/oauth2/authorize?client_id=991062751633883136&permissions=2416176272&integration_type=0&scope=bot', '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hero-section {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120px 20px 80px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-text {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 4rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: 24px;
|
||||
line-height: 1.2;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
background: linear-gradient(135deg, #fff, var(--color-secondary, #ff5252));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.25rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-bottom: 32px;
|
||||
line-height: 1.6;
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 48px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.hero-btn {
|
||||
padding: 14px 32px;
|
||||
border-radius: 30px;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.hero-btn.primary {
|
||||
background: var(--gradient-primary, linear-gradient(135deg, #ff1744, #d50000));
|
||||
color: white;
|
||||
box-shadow: 0 8px 30px var(--color-glow, rgba(255, 23, 68, 0.4));
|
||||
}
|
||||
|
||||
.hero-btn.primary:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 12px 40px var(--color-glow, rgba(255, 23, 68, 0.6));
|
||||
}
|
||||
|
||||
.hero-btn.secondary {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: white;
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.hero-btn.secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 968px) {
|
||||
.hero-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.hero-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hero-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
340
AmayoWeb/src/components/docs/IslandNavbar.vue
Normal file
340
AmayoWeb/src/components/docs/IslandNavbar.vue
Normal file
@@ -0,0 +1,340 @@
|
||||
<template>
|
||||
<nav class="island-navbar">
|
||||
<div class="navbar-content">
|
||||
<!-- Logo Section -->
|
||||
<div class="logo-section">
|
||||
<div class="bot-avatar">
|
||||
<img :src="favicon" alt="Amayo Bot" />
|
||||
</div>
|
||||
<span class="bot-name">{{ botName }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Actions Section -->
|
||||
<div class="actions-section">
|
||||
<!-- Theme Selector Dropdown -->
|
||||
<div class="theme-dropdown" ref="themeDropdown">
|
||||
<button class="theme-toggle-btn" @click="toggleThemeMenu">
|
||||
<div class="current-theme-preview" :style="{ background: getCurrentThemeGradient() }"></div>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="white">
|
||||
<path d="M2 4l4 4 4-4" stroke="currentColor" stroke-width="2" fill="none" />
|
||||
</svg>
|
||||
</button>
|
||||
<div v-show="showThemeMenu" class="theme-menu">
|
||||
<button
|
||||
v-for="theme in themes"
|
||||
:key="theme.name"
|
||||
:class="['theme-menu-item', { active: currentTheme === theme.name }]"
|
||||
@click="changeTheme(theme.name)"
|
||||
>
|
||||
<div class="theme-preview" :style="{ background: theme.gradient }"></div>
|
||||
<span>{{ t(`themes.${theme.name}`) }}</span>
|
||||
<svg v-if="currentTheme === theme.name" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M13 4L6 11L3 8" stroke="#00e676" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Language Selector -->
|
||||
<button class="lang-btn" @click="toggleLanguage">
|
||||
{{ currentLang === 'es' ? '🇪🇸' : '🇺🇸' }}
|
||||
</button>
|
||||
|
||||
<!-- Navigation Buttons -->
|
||||
<a href="/dashboard" class="nav-btn primary">
|
||||
{{ t('navbar.dashboard') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const favicon = ref('https://docs.amayo.dev/favicon.ico') // Reemplaza con el avatar real del bot
|
||||
const botName = ref('Amayo')
|
||||
|
||||
const currentTheme = ref('red')
|
||||
const currentLang = computed(() => locale.value)
|
||||
const showThemeMenu = ref(false)
|
||||
const themeDropdown = ref(null)
|
||||
|
||||
const themes = [
|
||||
{ name: 'red', gradient: 'linear-gradient(135deg, #ff1744, #d50000)' },
|
||||
{ name: 'blue', gradient: 'linear-gradient(135deg, #2196f3, #1565c0)' },
|
||||
{ name: 'green', gradient: 'linear-gradient(135deg, #00e676, #00c853)' },
|
||||
{ name: 'purple', gradient: 'linear-gradient(135deg, #e040fb, #9c27b0)' },
|
||||
{ name: 'orange', gradient: 'linear-gradient(135deg, #ff9100, #ff6d00)' },
|
||||
]
|
||||
|
||||
const getCurrentThemeGradient = () => {
|
||||
const theme = themes.find(t => t.name === currentTheme.value)
|
||||
return theme ? theme.gradient : themes[0].gradient
|
||||
}
|
||||
|
||||
const toggleThemeMenu = () => {
|
||||
showThemeMenu.value = !showThemeMenu.value
|
||||
}
|
||||
|
||||
const changeTheme = (themeName) => {
|
||||
currentTheme.value = themeName
|
||||
document.documentElement.setAttribute('data-theme', themeName)
|
||||
localStorage.setItem('theme', themeName)
|
||||
showThemeMenu.value = false
|
||||
}
|
||||
|
||||
const toggleLanguage = () => {
|
||||
locale.value = locale.value === 'es' ? 'en' : 'es'
|
||||
localStorage.setItem('language', locale.value)
|
||||
}
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (themeDropdown.value && !themeDropdown.value.contains(event.target)) {
|
||||
showThemeMenu.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
|
||||
const savedTheme = localStorage.getItem('theme')
|
||||
const savedLang = localStorage.getItem('language')
|
||||
|
||||
if (savedTheme) {
|
||||
currentTheme.value = savedTheme
|
||||
document.documentElement.setAttribute('data-theme', savedTheme)
|
||||
}
|
||||
|
||||
if (savedLang) {
|
||||
locale.value = savedLang
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.island-navbar {
|
||||
position: fixed;
|
||||
top: 25px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1000;
|
||||
width: 90%;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.navbar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 24px;
|
||||
background: rgba(10, 10, 10, 0.8);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bot-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 2px solid var(--color-primary, #ff1744);
|
||||
box-shadow: 0 0 20px var(--color-glow, rgba(255, 23, 68, 0.3));
|
||||
}
|
||||
|
||||
.bot-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.bot-name {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
background: var(--gradient-primary, linear-gradient(135deg, #ff1744, #ff5252));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.actions-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.theme-dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.theme-toggle-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.theme-toggle-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.current-theme-preview {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.theme-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
background: rgba(10, 10, 10, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 8px;
|
||||
min-width: 200px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.theme-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: white;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.theme-menu-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.theme-menu-item.active {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.theme-preview {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.theme-menu-item span {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.lang-btn {
|
||||
font-size: 1.5rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.lang-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
padding: 10px 24px;
|
||||
border-radius: 25px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.nav-btn.primary {
|
||||
background: var(--gradient-primary, linear-gradient(135deg, #ff1744, #d50000));
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px var(--color-glow, rgba(255, 23, 68, 0.4));
|
||||
}
|
||||
|
||||
.nav-btn.primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px var(--color-glow, rgba(255, 23, 68, 0.6));
|
||||
}
|
||||
|
||||
.nav-btn.secondary {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.nav-btn.secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.navbar-content {
|
||||
padding: 10px 16px;
|
||||
}
|
||||
|
||||
.bot-name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.theme-dropdown {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
padding: 8px 16px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.island-navbar {
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1000;
|
||||
width: 85%;
|
||||
max-width: 1200px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -5,7 +5,7 @@ export default {
|
||||
dashboard: 'Panel',
|
||||
},
|
||||
hero: {
|
||||
subtitle: 'Transforma tu servidor de Discord en una experiencia RPG única con minijuegos, economía, y mucho más',
|
||||
subtitle: 'Transforma tu servidor de Discord en una experiencia de Ultima Generacion de comandos con nuevas tecnologias.',
|
||||
exploreFeatures: 'Explorar Características',
|
||||
inviteBot: 'Invitar Bot',
|
||||
servers: 'Servidores',
|
||||
@@ -15,6 +15,37 @@ export default {
|
||||
feature2: 'Tickets',
|
||||
feature3: 'AutoMod',
|
||||
},
|
||||
hero_docs: {
|
||||
subtitle: 'En esta seccion esta la documentacion oficial para Amayo Bot.',
|
||||
exploreFeatures: 'Ver Comandos',
|
||||
inviteBot: 'Ver Funciones',
|
||||
},
|
||||
docs: {
|
||||
sections: 'Funciones',
|
||||
getStarted: 'GET STARTED',
|
||||
introduction: 'Introduction',
|
||||
modules: 'MODULES',
|
||||
drops: 'Drops',
|
||||
economy: 'Economy',
|
||||
moderation: 'Moderation',
|
||||
utilities: 'Utilities',
|
||||
alliances: 'Alliances',
|
||||
other: 'OTHER',
|
||||
settings: 'Settings',
|
||||
support: 'Support',
|
||||
introText: 'En esta sección está la documentación oficial para Amayo Bot.',
|
||||
inviteBot: 'Invite Amayo to your server',
|
||||
joinSupport: 'Join the support server',
|
||||
privacyPolicy: 'Privacy Policy',
|
||||
termsOfService: 'Terms of Service',
|
||||
defaultPrefix: 'The default prefix is',
|
||||
prefixInfo: 'which can be changed by using !settings',
|
||||
createDrops: 'Create Drops',
|
||||
dropsDescription: 'Add excitement to your server with our drop generation system.',
|
||||
utilitiesDescription: 'Implement utilities into your server to add more customizability.',
|
||||
economyDescription: 'Spice up your server by rewarding members for using your server.',
|
||||
moderationDescription: 'Protect your server from bad actors by using our moderation tools.',
|
||||
},
|
||||
login: {
|
||||
title: 'Iniciar Sesión',
|
||||
withDiscord: 'Continuar con Discord',
|
||||
@@ -35,7 +66,7 @@ export default {
|
||||
dashboard: 'Dashboard',
|
||||
},
|
||||
hero: {
|
||||
subtitle: 'Transform your Discord server into a unique RPG experience with minigames, economy, and much more',
|
||||
subtitle: 'Transform your Discord server into a Next-Gen command experience with cutting-edge technologies.',
|
||||
exploreFeatures: 'Explore Features',
|
||||
inviteBot: 'Invite Bot',
|
||||
servers: 'Servers',
|
||||
@@ -44,6 +75,37 @@ export default {
|
||||
feature1: 'Alliances',
|
||||
feature2: 'Tickets',
|
||||
feature3: 'AutoMod',
|
||||
},
|
||||
hero_docs: {
|
||||
subtitle: 'This section contains the official documentation for Amayo Bot.',
|
||||
exploreFeatures: 'View Commands',
|
||||
inviteBot: 'View Functions',
|
||||
},
|
||||
docs: {
|
||||
sections: 'Functions',
|
||||
getStarted: 'GET STARTED',
|
||||
introduction: 'Introduction',
|
||||
modules: 'MODULES',
|
||||
drops: 'Drops',
|
||||
economy: 'Economy',
|
||||
moderation: 'Moderation',
|
||||
utilities: 'Utilities',
|
||||
alliances: 'Alliances',
|
||||
other: 'OTHER',
|
||||
settings: 'Settings',
|
||||
support: 'Support',
|
||||
introText: 'This section contains the official documentation for Amayo Bot.',
|
||||
inviteBot: 'Invite Amayo to your server',
|
||||
joinSupport: 'Join the support server',
|
||||
privacyPolicy: 'Privacy Policy',
|
||||
termsOfService: 'Terms of Service',
|
||||
defaultPrefix: 'The default prefix is',
|
||||
prefixInfo: 'which can be changed by using !settings',
|
||||
createDrops: 'Create Drops',
|
||||
dropsDescription: 'Add excitement to your server with our drop generation system.',
|
||||
utilitiesDescription: 'Implement utilities into your server to add more customizability.',
|
||||
economyDescription: 'Spice up your server by rewarding members for using your server.',
|
||||
moderationDescription: 'Protect your server from bad actors by using our moderation tools.',
|
||||
},
|
||||
login: {
|
||||
title: 'Sign In',
|
||||
|
||||
@@ -4,18 +4,31 @@ import AuthCallback from '../views/AuthCallback.vue'
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: () => import('../views/HomeView.vue')
|
||||
},
|
||||
{
|
||||
path: '/docs',
|
||||
name: 'docs',
|
||||
component: () => import('../views/DocsView.vue')
|
||||
},
|
||||
{
|
||||
path: '/auth/callback',
|
||||
name: 'auth-callback',
|
||||
component: AuthCallback
|
||||
},
|
||||
// Agregar más rutas según sea necesario
|
||||
// {
|
||||
// path: '/dashboard',
|
||||
// name: 'dashboard',
|
||||
// component: () => import('../views/Dashboard.vue'),
|
||||
// meta: { requiresAuth: true }
|
||||
// }
|
||||
{
|
||||
path: '/terms',
|
||||
name: 'terms',
|
||||
component: () => import('../views/TermsOfService.vue')
|
||||
},
|
||||
{
|
||||
path: '/privacy',
|
||||
name: 'privacy',
|
||||
component: () => import('../views/PrivacyPolicy.vue')
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
@@ -1,37 +1,121 @@
|
||||
import axios from 'axios'
|
||||
import { securityService, rateLimiter } from './security'
|
||||
|
||||
const API_URL = import.meta.env.PROD
|
||||
? 'https://api.amayo.dev/api'
|
||||
: 'http://localhost:3000/api'
|
||||
// Inicializar servicio de seguridad
|
||||
await securityService.initialize().catch(err => {
|
||||
console.error('Failed to initialize security:', err)
|
||||
})
|
||||
|
||||
// Crear instancia de axios con configuración de seguridad
|
||||
const createSecureAxios = () => {
|
||||
const instance = axios.create({
|
||||
timeout: 10000, // 10 segundos timeout
|
||||
headers: securityService.getSecurityHeaders()
|
||||
})
|
||||
|
||||
// Interceptor para agregar headers de seguridad
|
||||
instance.interceptors.request.use(
|
||||
config => {
|
||||
config.headers = {
|
||||
...config.headers,
|
||||
...securityService.getSecurityHeaders()
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => Promise.reject(error)
|
||||
)
|
||||
|
||||
// Interceptor para validar respuestas
|
||||
instance.interceptors.response.use(
|
||||
response => securityService.validateResponse(response),
|
||||
error => {
|
||||
// Manejar errores de forma segura
|
||||
if (error.response?.status === 429) {
|
||||
console.error('Rate limit exceeded')
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
const secureAxios = createSecureAxios()
|
||||
|
||||
// No exponer la URL directamente - usar el servicio de seguridad
|
||||
const getApiUrl = (path) => {
|
||||
try {
|
||||
const baseUrl = securityService.getApiEndpoint()
|
||||
return `${baseUrl}${path}`
|
||||
} catch (error) {
|
||||
console.error('Failed to get API URL:', error)
|
||||
throw new Error('API service unavailable')
|
||||
}
|
||||
}
|
||||
|
||||
export const authService = {
|
||||
// Redirigir al usuario a Discord OAuth2
|
||||
loginWithDiscord() {
|
||||
// Rate limiting para prevenir abuso
|
||||
if (!rateLimiter.canMakeRequest('/auth/discord', 'auth')) {
|
||||
const remainingTime = Math.ceil(rateLimiter.getRemainingTime('/auth/discord', 'auth') / 1000)
|
||||
throw new Error(`Too many login attempts. Please wait ${remainingTime} seconds.`)
|
||||
}
|
||||
|
||||
const clientId = import.meta.env.VITE_DISCORD_CLIENT_ID
|
||||
if (!clientId) {
|
||||
throw new Error('Discord client ID not configured')
|
||||
}
|
||||
|
||||
const redirectUri = import.meta.env.PROD
|
||||
? 'https://docs.amayo.dev/auth/callback'
|
||||
? window.location.origin + '/auth/callback'
|
||||
: 'http://localhost:5173/auth/callback'
|
||||
|
||||
const scope = 'identify guilds'
|
||||
const authUrl = `https://discord.com/api/oauth2/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}`
|
||||
const state = securityService.generateSessionToken() // CSRF protection
|
||||
|
||||
// Guardar state para validación
|
||||
sessionStorage.setItem('oauth_state', state)
|
||||
|
||||
const authUrl = `https://discord.com/api/oauth2/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}&state=${state}`
|
||||
|
||||
window.location.href = authUrl
|
||||
},
|
||||
|
||||
// Intercambiar código por token
|
||||
async handleCallback(code) {
|
||||
async handleCallback(code, state) {
|
||||
// Validar state para prevenir CSRF
|
||||
const savedState = sessionStorage.getItem('oauth_state')
|
||||
if (state !== savedState) {
|
||||
throw new Error('Invalid OAuth state - possible CSRF attack')
|
||||
}
|
||||
sessionStorage.removeItem('oauth_state')
|
||||
|
||||
// Rate limiting
|
||||
if (!rateLimiter.canMakeRequest('/auth/callback', 'auth')) {
|
||||
throw new Error('Too many authentication attempts')
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${API_URL}/auth/discord/callback`, { code })
|
||||
const response = await secureAxios.post(
|
||||
getApiUrl('/auth/discord/callback'),
|
||||
{ code, state }
|
||||
)
|
||||
|
||||
const { token, user } = response.data
|
||||
|
||||
// Guardar token en localStorage
|
||||
if (!token || !user) {
|
||||
throw new Error('Invalid authentication response')
|
||||
}
|
||||
|
||||
// Guardar token de forma segura
|
||||
localStorage.setItem('authToken', token)
|
||||
localStorage.setItem('user', JSON.stringify(user))
|
||||
|
||||
return { token, user }
|
||||
} catch (error) {
|
||||
console.error('Error during authentication:', error)
|
||||
throw error
|
||||
console.error('Authentication error:', error)
|
||||
throw new Error('Authentication failed')
|
||||
}
|
||||
},
|
||||
|
||||
@@ -40,16 +124,22 @@ export const authService = {
|
||||
const token = localStorage.getItem('authToken')
|
||||
if (!token) return null
|
||||
|
||||
// Rate limiting
|
||||
if (!rateLimiter.canMakeRequest('/auth/me', 'api')) {
|
||||
throw new Error('Too many requests')
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/auth/me`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
const response = await secureAxios.get(getApiUrl('/auth/me'))
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Error fetching user:', error)
|
||||
this.logout()
|
||||
|
||||
// Si el token es inválido, hacer logout
|
||||
if (error.response?.status === 401) {
|
||||
this.logout()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
},
|
||||
@@ -58,12 +148,29 @@ export const authService = {
|
||||
logout() {
|
||||
localStorage.removeItem('authToken')
|
||||
localStorage.removeItem('user')
|
||||
securityService.clearSensitiveData()
|
||||
window.location.href = '/'
|
||||
},
|
||||
|
||||
// Verificar si el usuario está autenticado
|
||||
isAuthenticated() {
|
||||
return !!localStorage.getItem('authToken')
|
||||
const token = localStorage.getItem('authToken')
|
||||
if (!token) return false
|
||||
|
||||
// Validar que el token no esté expirado (básico)
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.split('.')[1]))
|
||||
const isExpired = payload.exp && payload.exp * 1000 < Date.now()
|
||||
|
||||
if (isExpired) {
|
||||
this.logout()
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
} catch {
|
||||
return !!token // Fallback si no se puede decodificar
|
||||
}
|
||||
},
|
||||
|
||||
// Obtener token
|
||||
|
||||
@@ -1,19 +1,70 @@
|
||||
import axios from 'axios'
|
||||
import { securityService, rateLimiter } from './security'
|
||||
|
||||
const API_URL = import.meta.env.PROD
|
||||
? 'https://api.amayo.dev'
|
||||
: 'http://localhost:3000'
|
||||
// Inicializar servicio de seguridad
|
||||
await securityService.initialize().catch(err => {
|
||||
console.error('Failed to initialize security:', err)
|
||||
})
|
||||
|
||||
// Crear instancia de axios con configuración de seguridad
|
||||
const createSecureAxios = () => {
|
||||
const instance = axios.create({
|
||||
timeout: 10000,
|
||||
headers: securityService.getSecurityHeaders()
|
||||
})
|
||||
|
||||
instance.interceptors.request.use(
|
||||
config => {
|
||||
config.headers = {
|
||||
...config.headers,
|
||||
...securityService.getSecurityHeaders()
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => Promise.reject(error)
|
||||
)
|
||||
|
||||
instance.interceptors.response.use(
|
||||
response => securityService.validateResponse(response),
|
||||
error => Promise.reject(error)
|
||||
)
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
const secureAxios = createSecureAxios()
|
||||
|
||||
const getApiUrl = (path) => {
|
||||
try {
|
||||
const baseUrl = securityService.getApiEndpoint()
|
||||
return `${baseUrl}${path}`
|
||||
} catch (error) {
|
||||
console.error('Failed to get API URL:', error)
|
||||
throw new Error('API service unavailable')
|
||||
}
|
||||
}
|
||||
|
||||
export const botService = {
|
||||
// Obtener estadísticas del bot
|
||||
async getStats() {
|
||||
// Rate limiting
|
||||
if (!rateLimiter.canMakeRequest('/bot/stats', 'api')) {
|
||||
console.warn('Rate limit reached for bot stats')
|
||||
return this.getCachedStats()
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/api/bot/stats`)
|
||||
const response = await secureAxios.get(getApiUrl('/bot/stats'))
|
||||
|
||||
// Cachear los resultados
|
||||
this.cacheStats(response.data)
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Error fetching bot stats:', error)
|
||||
// Retornar valores por defecto en caso de error
|
||||
return {
|
||||
|
||||
// Retornar stats cacheadas si falló la petición
|
||||
return this.getCachedStats() || {
|
||||
servers: 0,
|
||||
users: 0,
|
||||
commands: 0
|
||||
@@ -23,11 +74,88 @@ export const botService = {
|
||||
|
||||
// Obtener información del bot (nombre, avatar, etc.)
|
||||
async getBotInfo() {
|
||||
// Rate limiting
|
||||
if (!rateLimiter.canMakeRequest('/bot/info', 'api')) {
|
||||
return this.getCachedBotInfo()
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/api/bot/info`)
|
||||
const response = await secureAxios.get(getApiUrl('/bot/info'))
|
||||
|
||||
// Cachear info del bot
|
||||
this.cacheBotInfo(response.data)
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Error fetching bot info:', error)
|
||||
return this.getCachedBotInfo()
|
||||
}
|
||||
},
|
||||
|
||||
// Sistema de caché para stats
|
||||
cacheStats(stats) {
|
||||
try {
|
||||
const cacheData = {
|
||||
data: stats,
|
||||
timestamp: Date.now(),
|
||||
expiresIn: 5 * 60 * 1000 // 5 minutos
|
||||
}
|
||||
sessionStorage.setItem('bot_stats_cache', JSON.stringify(cacheData))
|
||||
} catch (error) {
|
||||
console.error('Failed to cache stats:', error)
|
||||
}
|
||||
},
|
||||
|
||||
getCachedStats() {
|
||||
try {
|
||||
const cached = sessionStorage.getItem('bot_stats_cache')
|
||||
if (!cached) return null
|
||||
|
||||
const cacheData = JSON.parse(cached)
|
||||
const isExpired = Date.now() - cacheData.timestamp > cacheData.expiresIn
|
||||
|
||||
if (isExpired) {
|
||||
sessionStorage.removeItem('bot_stats_cache')
|
||||
return null
|
||||
}
|
||||
|
||||
return cacheData.data
|
||||
} catch (error) {
|
||||
console.error('Failed to get cached stats:', error)
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
// Sistema de caché para bot info
|
||||
cacheBotInfo(info) {
|
||||
try {
|
||||
const cacheData = {
|
||||
data: info,
|
||||
timestamp: Date.now(),
|
||||
expiresIn: 60 * 60 * 1000 // 1 hora
|
||||
}
|
||||
sessionStorage.setItem('bot_info_cache', JSON.stringify(cacheData))
|
||||
} catch (error) {
|
||||
console.error('Failed to cache bot info:', error)
|
||||
}
|
||||
},
|
||||
|
||||
getCachedBotInfo() {
|
||||
try {
|
||||
const cached = sessionStorage.getItem('bot_info_cache')
|
||||
if (!cached) return null
|
||||
|
||||
const cacheData = JSON.parse(cached)
|
||||
const isExpired = Date.now() - cacheData.timestamp > cacheData.expiresIn
|
||||
|
||||
if (isExpired) {
|
||||
sessionStorage.removeItem('bot_info_cache')
|
||||
return null
|
||||
}
|
||||
|
||||
return cacheData.data
|
||||
} catch (error) {
|
||||
console.error('Failed to get cached bot info:', error)
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
167
AmayoWeb/src/services/security.js
Normal file
167
AmayoWeb/src/services/security.js
Normal file
@@ -0,0 +1,167 @@
|
||||
// Security Configuration Service
|
||||
// Este servicio maneja la configuración de seguridad del cliente
|
||||
// y protege el acceso al backend
|
||||
|
||||
class SecurityService {
|
||||
constructor() {
|
||||
this.initialized = false;
|
||||
this.sessionToken = null;
|
||||
this.apiEndpoint = null;
|
||||
}
|
||||
|
||||
// Inicializar configuración de seguridad
|
||||
async initialize() {
|
||||
if (this.initialized) return;
|
||||
|
||||
try {
|
||||
// En producción, obtener configuración del servidor de forma segura
|
||||
// Esto evita hardcodear URLs en el código del cliente
|
||||
if (import.meta.env.PROD) {
|
||||
// Obtener configuración inicial del servidor mediante un endpoint público
|
||||
// que solo devuelve información necesaria sin revelar detalles del backend
|
||||
const config = await this.fetchSecureConfig();
|
||||
this.apiEndpoint = config.endpoint;
|
||||
} else {
|
||||
// En desarrollo, usar localhost
|
||||
this.apiEndpoint = 'http://localhost:3000/api';
|
||||
}
|
||||
|
||||
// Generar un token de sesión único
|
||||
this.sessionToken = this.generateSessionToken();
|
||||
this.initialized = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize security service:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Obtener configuración segura del servidor
|
||||
async fetchSecureConfig() {
|
||||
// Este endpoint debe estar protegido con Cloudflare y rate limiting
|
||||
// y solo devolver el endpoint de API sin revelar la IP del servidor
|
||||
const response = await fetch('/.well-known/api-config.json', {
|
||||
headers: {
|
||||
'X-Client-Version': import.meta.env.VITE_APP_VERSION || '1.0.0',
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch API configuration');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// Generar token de sesión único
|
||||
generateSessionToken() {
|
||||
const array = new Uint8Array(32);
|
||||
crypto.getRandomValues(array);
|
||||
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
// Obtener el endpoint de la API de forma segura
|
||||
getApiEndpoint() {
|
||||
if (!this.initialized) {
|
||||
throw new Error('Security service not initialized');
|
||||
}
|
||||
return this.apiEndpoint;
|
||||
}
|
||||
|
||||
// Obtener headers de seguridad para requests
|
||||
getSecurityHeaders() {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Client-Token': this.sessionToken,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
};
|
||||
|
||||
// Agregar timestamp para prevenir replay attacks
|
||||
headers['X-Timestamp'] = Date.now().toString();
|
||||
|
||||
// Agregar auth token si existe
|
||||
const authToken = localStorage.getItem('authToken');
|
||||
if (authToken) {
|
||||
headers['Authorization'] = `Bearer ${authToken}`;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
// Validar respuesta del servidor
|
||||
validateResponse(response) {
|
||||
// Verificar headers de seguridad en la respuesta
|
||||
const serverToken = response.headers.get('X-Server-Token');
|
||||
if (!serverToken) {
|
||||
console.warn('Missing server security token');
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// Limpiar datos sensibles
|
||||
clearSensitiveData() {
|
||||
this.sessionToken = null;
|
||||
this.apiEndpoint = null;
|
||||
this.initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Exportar instancia singleton
|
||||
export const securityService = new SecurityService();
|
||||
|
||||
// Rate limiting client-side
|
||||
class RateLimiter {
|
||||
constructor() {
|
||||
this.requests = new Map();
|
||||
this.limits = {
|
||||
default: { maxRequests: 10, windowMs: 60000 }, // 10 requests por minuto
|
||||
auth: { maxRequests: 3, windowMs: 60000 }, // 3 intentos de login por minuto
|
||||
api: { maxRequests: 30, windowMs: 60000 }, // 30 API calls por minuto
|
||||
};
|
||||
}
|
||||
|
||||
canMakeRequest(endpoint, type = 'default') {
|
||||
const now = Date.now();
|
||||
const key = `${type}:${endpoint}`;
|
||||
const limit = this.limits[type] || this.limits.default;
|
||||
|
||||
if (!this.requests.has(key)) {
|
||||
this.requests.set(key, []);
|
||||
}
|
||||
|
||||
const requests = this.requests.get(key);
|
||||
|
||||
// Limpiar requests antiguos
|
||||
const validRequests = requests.filter(
|
||||
timestamp => now - timestamp < limit.windowMs
|
||||
);
|
||||
|
||||
this.requests.set(key, validRequests);
|
||||
|
||||
// Verificar si se puede hacer el request
|
||||
if (validRequests.length >= limit.maxRequests) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Registrar nuevo request
|
||||
validRequests.push(now);
|
||||
this.requests.set(key, validRequests);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getRemainingTime(endpoint, type = 'default') {
|
||||
const key = `${type}:${endpoint}`;
|
||||
const requests = this.requests.get(key) || [];
|
||||
|
||||
if (requests.length === 0) return 0;
|
||||
|
||||
const limit = this.limits[type] || this.limits.default;
|
||||
const oldestRequest = Math.min(...requests);
|
||||
const timeUntilReset = limit.windowMs - (Date.now() - oldestRequest);
|
||||
|
||||
return Math.max(0, timeUntilReset);
|
||||
}
|
||||
}
|
||||
|
||||
export const rateLimiter = new RateLimiter();
|
||||
466
AmayoWeb/src/views/DocsView.vue
Normal file
466
AmayoWeb/src/views/DocsView.vue
Normal file
@@ -0,0 +1,466 @@
|
||||
<template>
|
||||
<div class="docs-view">
|
||||
<AnimatedBackground />
|
||||
|
||||
<div class="docs-header">
|
||||
<IslandNavbar />
|
||||
<HeroSection />
|
||||
</div>
|
||||
|
||||
<!-- Contenido principal -->
|
||||
<div class="docs-body">
|
||||
<!-- Sidebar permanente -->
|
||||
<aside class="docs-sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
<h3>{{ t('docs.sections') }}</h3>
|
||||
|
||||
<div class="nav-section">
|
||||
<h4>{{ t('docs.getStarted') }}</h4>
|
||||
<a href="#introduction" @click.prevent="scrollToSection('introduction')" :class="{ active: activeSection === 'introduction' }">
|
||||
📖 {{ t('docs.introduction') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h4>{{ t('docs.modules') }}</h4>
|
||||
<a href="#drops" @click.prevent="scrollToSection('drops')" :class="{ active: activeSection === 'drops' }">
|
||||
🎁 {{ t('docs.drops') }}
|
||||
</a>
|
||||
<a href="#economy" @click.prevent="scrollToSection('economy')" :class="{ active: activeSection === 'economy' }">
|
||||
💰 {{ t('docs.economy') }}
|
||||
</a>
|
||||
<a href="#moderation" @click.prevent="scrollToSection('moderation')" :class="{ active: activeSection === 'moderation' }">
|
||||
🛡️ {{ t('docs.moderation') }}
|
||||
</a>
|
||||
<a href="#utilities" @click.prevent="scrollToSection('utilities')" :class="{ active: activeSection === 'utilities' }">
|
||||
🔧 {{ t('docs.utilities') }}
|
||||
</a>
|
||||
<a href="#alliances" @click.prevent="scrollToSection('alliances')" :class="{ active: activeSection === 'alliances' }">
|
||||
🤝 {{ t('docs.alliances') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h4>{{ t('docs.other') }}</h4>
|
||||
<a href="#settings" @click.prevent="scrollToSection('settings')" :class="{ active: activeSection === 'settings' }">
|
||||
⚙️ {{ t('docs.settings') }}
|
||||
</a>
|
||||
<a href="#support" @click.prevent="scrollToSection('support')" :class="{ active: activeSection === 'support' }">
|
||||
💬 {{ t('docs.support') }}
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div class="docs-content" ref="docsContent">
|
||||
<div class="docs-container">
|
||||
<!-- Introduction Section -->
|
||||
<section id="introduction" class="doc-section">
|
||||
<h1>{{ t('docs.introduction') }}</h1>
|
||||
<p class="intro">{{ t('docs.introText') }}</p>
|
||||
|
||||
<div class="info-cards">
|
||||
<div class="info-card">
|
||||
<h3>• {{ t('docs.inviteBot') }}</h3>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<h3>• {{ t('docs.joinSupport') }}</h3>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<h3>• {{ t('docs.privacyPolicy') }}</h3>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<h3>• {{ t('docs.termsOfService') }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="highlight-box">
|
||||
<div class="highlight-icon">💡</div>
|
||||
<div class="highlight-content">
|
||||
<strong>{{ t('docs.defaultPrefix') }}:</strong> {{ t('docs.prefixInfo') }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Module Sections -->
|
||||
<section id="drops" class="doc-section module-section">
|
||||
<div class="section-header">
|
||||
<span class="section-icon">🎁</span>
|
||||
<h2>{{ t('docs.createDrops') }}</h2>
|
||||
</div>
|
||||
<p>{{ t('docs.dropsDescription') }}</p>
|
||||
</section>
|
||||
|
||||
<section id="utilities" class="doc-section module-section">
|
||||
<div class="section-header">
|
||||
<span class="section-icon">🔧</span>
|
||||
<h2>{{ t('docs.utilities') }}</h2>
|
||||
</div>
|
||||
<p>{{ t('docs.utilitiesDescription') }}</p>
|
||||
</section>
|
||||
|
||||
<section id="economy" class="doc-section module-section">
|
||||
<div class="section-header">
|
||||
<span class="section-icon">💰</span>
|
||||
<h2>{{ t('docs.economy') }}</h2>
|
||||
</div>
|
||||
<p>{{ t('docs.economyDescription') }}</p>
|
||||
</section>
|
||||
|
||||
<section id="moderation" class="doc-section module-section">
|
||||
<div class="section-header">
|
||||
<span class="section-icon">🛡️</span>
|
||||
<h2>{{ t('docs.moderation') }}</h2>
|
||||
</div>
|
||||
<p>{{ t('docs.moderationDescription') }}</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useTheme } from '../composables/useTheme';
|
||||
import AnimatedBackground from '../components/AnimatedBackground.vue';
|
||||
import IslandNavbar from '../components/docs/IslandNavbar.vue';
|
||||
import HeroSection from '../components/docs/HeroSection.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { initTheme } = useTheme();
|
||||
const activeSection = ref('introduction');
|
||||
const docsContent = ref<HTMLElement | null>(null);
|
||||
|
||||
const scrollToSection = (sectionId: string) => {
|
||||
const element = document.getElementById(sectionId);
|
||||
const container = docsContent.value;
|
||||
if (element && container) {
|
||||
// calcular posición relativa dentro del contenedor scrollable
|
||||
const elemRect = element.getBoundingClientRect();
|
||||
const contRect = container.getBoundingClientRect();
|
||||
const offset = elemRect.top - contRect.top + container.scrollTop;
|
||||
container.scrollTo({ top: offset - 16, behavior: 'smooth' });
|
||||
activeSection.value = sectionId;
|
||||
return;
|
||||
}
|
||||
|
||||
// fallback al comportamiento global
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
activeSection.value = sectionId;
|
||||
}
|
||||
};
|
||||
|
||||
// Detectar sección activa con scroll (dentro del contenedor docsContent)
|
||||
const handleScroll = () => {
|
||||
const sections = ['introduction', 'drops', 'economy', 'moderation', 'utilities', 'alliances', 'settings', 'support'];
|
||||
const container = docsContent.value;
|
||||
if (!container) {
|
||||
// fallback a window
|
||||
for (const sectionId of sections) {
|
||||
const element = document.getElementById(sectionId);
|
||||
if (element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (rect.top >= 0 && rect.top < window.innerHeight / 2) {
|
||||
activeSection.value = sectionId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const contRect = container.getBoundingClientRect();
|
||||
for (const sectionId of sections) {
|
||||
const element = document.getElementById(sectionId);
|
||||
if (element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const top = rect.top - contRect.top; // posición relativa dentro del contenedor
|
||||
if (top >= 0 && top < container.clientHeight / 2) {
|
||||
activeSection.value = sectionId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initTheme();
|
||||
// si existe el contenedor de docs, listen al scroll interno
|
||||
if (docsContent.value) {
|
||||
docsContent.value.addEventListener('scroll', handleScroll, { passive: true });
|
||||
// inicializar estado
|
||||
handleScroll();
|
||||
} else {
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (docsContent.value) {
|
||||
docsContent.value.removeEventListener('scroll', handleScroll);
|
||||
} else {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.docs-view {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.docs-header {
|
||||
width: 100%;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/* Contenedor principal que agrupa sidebar + contenido */
|
||||
.docs-body {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
/* Sidebar Fijo */
|
||||
.docs-sidebar {
|
||||
position: sticky;
|
||||
left: 20px;
|
||||
top: 120px;
|
||||
width: 240px;
|
||||
height: calc(100vh - 140px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.sidebar-nav h3 {
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.nav-section h4 {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sidebar-nav a {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 4px;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.sidebar-nav a:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.sidebar-nav a.active {
|
||||
background: var(--gradient-primary);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Contenido principal con compensación automática */
|
||||
.docs-content {
|
||||
width: 100%;
|
||||
/* convertir en contenedor scrollable independiente */
|
||||
max-height: calc(100vh - 140px);
|
||||
overflow-y: auto;
|
||||
padding-left: 24%; /* reserva espacio para el sidebar */
|
||||
padding-right: 40px;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.docs-container {
|
||||
max-width: 900px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
.doc-section {
|
||||
padding: 60px 0;
|
||||
scroll-margin-top: 100px;
|
||||
}
|
||||
|
||||
.doc-section h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 16px;
|
||||
background: linear-gradient(135deg, #fff, #ff5252);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.intro {
|
||||
font-size: 1.1rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-bottom: 32px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Info Cards */
|
||||
.info-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.info-card:hover {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.info-card h3 {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Highlight Box */
|
||||
.highlight-box {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
background: rgba(0, 230, 118, 0.05);
|
||||
border: 1px solid rgba(0, 230, 118, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin: 32px 0;
|
||||
}
|
||||
|
||||
.highlight-icon {
|
||||
font-size: 2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.highlight-content {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.highlight-content strong {
|
||||
color: #00e676;
|
||||
}
|
||||
|
||||
/* Module Sections */
|
||||
.module-section {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
font-size: 2.5rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 2rem;
|
||||
color: white;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.module-section > p {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
line-height: 1.6;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
/* Scrollbar personalizado */
|
||||
.docs-sidebar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.docs-sidebar::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.docs-sidebar::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.docs-sidebar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1200px) {
|
||||
.docs-sidebar {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.docs-content {
|
||||
padding-left: 260px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 968px) {
|
||||
.docs-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.docs-content {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.doc-section h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.info-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
20
AmayoWeb/src/views/HomeView.vue
Normal file
20
AmayoWeb/src/views/HomeView.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="home-view">
|
||||
<AnimatedBackground />
|
||||
<IslandNavbar />
|
||||
<HeroSection />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AnimatedBackground from '../components/AnimatedBackground.vue'
|
||||
import IslandNavbar from '../components/IslandNavbar.vue'
|
||||
import HeroSection from '../components/HeroSection.vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-view {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
}
|
||||
</style>
|
||||
349
AmayoWeb/src/views/PrivacyPolicy.vue
Normal file
349
AmayoWeb/src/views/PrivacyPolicy.vue
Normal file
@@ -0,0 +1,349 @@
|
||||
<template>
|
||||
<div class="legal-page">
|
||||
<AnimatedBackground />
|
||||
<IslandNavbar />
|
||||
|
||||
<div class="legal-container">
|
||||
<div class="legal-header">
|
||||
<h1>🔒 Privacy Policy</h1>
|
||||
<p class="last-updated">Last Updated: November 6, 2025</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-content">
|
||||
<section class="legal-section">
|
||||
<h2>1. Introduction</h2>
|
||||
<p>
|
||||
This Privacy Policy explains how Amayo Bot ("we", "us", or "our") collects, uses, and protects your personal
|
||||
information when you use our Discord bot. We are committed to ensuring the privacy and security of your data.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>2. Information We Collect</h2>
|
||||
<p>We collect the following types of information:</p>
|
||||
|
||||
<h3>2.1 Automatically Collected Data</h3>
|
||||
<ul>
|
||||
<li><strong>Discord User IDs:</strong> Unique identifiers provided by Discord</li>
|
||||
<li><strong>Discord Server IDs:</strong> Identifiers for servers where the bot is installed</li>
|
||||
<li><strong>Discord Channel IDs:</strong> For command execution and feature configuration</li>
|
||||
<li><strong>Command Usage Data:</strong> Information about which commands are used and when</li>
|
||||
</ul>
|
||||
|
||||
<h3>2.2 User-Provided Data</h3>
|
||||
<ul>
|
||||
<li><strong>Server Configuration:</strong> Settings you configure for your server</li>
|
||||
<li><strong>Alliance Data:</strong> Alliance names, points, and member information</li>
|
||||
<li><strong>Custom Content:</strong> Display components, custom commands, and configurations</li>
|
||||
<li><strong>Chat Messages:</strong> Messages sent to the AI chat feature (temporarily stored)</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>3. How We Use Your Information</h2>
|
||||
<p>We use the collected information for the following purposes:</p>
|
||||
<ul>
|
||||
<li>To provide and maintain the bot's functionality</li>
|
||||
<li>To personalize your experience with the bot</li>
|
||||
<li>To improve and optimize the bot's performance</li>
|
||||
<li>To analyze usage patterns and develop new features</li>
|
||||
<li>To respond to user inquiries and provide support</li>
|
||||
<li>To prevent abuse and ensure compliance with our Terms of Service</li>
|
||||
<li>To generate anonymous statistics and analytics</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>4. Data Storage and Security</h2>
|
||||
<p>
|
||||
We take the security of your data seriously and implement appropriate technical and organizational measures:
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Encryption:</strong> All data is encrypted in transit using industry-standard protocols</li>
|
||||
<li><strong>Secure Databases:</strong> Data is stored in secure, encrypted databases</li>
|
||||
<li><strong>Access Controls:</strong> Strict access controls limit who can access user data</li>
|
||||
<li><strong>Regular Backups:</strong> Data is backed up regularly to prevent loss</li>
|
||||
<li><strong>Monitoring:</strong> Systems are monitored for security threats and vulnerabilities</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>5. Data Retention</h2>
|
||||
<p>We retain different types of data for varying periods:</p>
|
||||
<ul>
|
||||
<li><strong>Server Configuration:</strong> Retained while the bot is in your server</li>
|
||||
<li><strong>Alliance Data:</strong> Retained indefinitely or until manual deletion</li>
|
||||
<li><strong>Command Logs:</strong> Retained for up to 90 days for analytics</li>
|
||||
<li><strong>AI Chat Messages:</strong> Retained temporarily for context (24-48 hours)</li>
|
||||
<li><strong>Error Logs:</strong> Retained for up to 30 days for debugging</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>6. Data Sharing and Third Parties</h2>
|
||||
<p>
|
||||
We do not sell, trade, or rent your personal information to third parties. We may share data only in the
|
||||
following circumstances:
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Discord API:</strong> We interact with Discord's services to provide bot functionality</li>
|
||||
<li><strong>AI Services:</strong> AI chat messages are processed by third-party AI providers (Google Gemini)</li>
|
||||
<li><strong>Hosting Providers:</strong> Our infrastructure is hosted on secure cloud platforms</li>
|
||||
<li><strong>Legal Requirements:</strong> When required by law or to protect our rights</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>7. Your Rights and Choices</h2>
|
||||
<p>You have the following rights regarding your data:</p>
|
||||
<ul>
|
||||
<li><strong>Access:</strong> Request a copy of your data</li>
|
||||
<li><strong>Correction:</strong> Request correction of inaccurate data</li>
|
||||
<li><strong>Deletion:</strong> Request deletion of your data (subject to certain limitations)</li>
|
||||
<li><strong>Opt-Out:</strong> Disable certain features or stop using the bot</li>
|
||||
<li><strong>Portability:</strong> Request your data in a portable format</li>
|
||||
</ul>
|
||||
<p>
|
||||
To exercise these rights, please contact us through our support server.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>8. Children's Privacy</h2>
|
||||
<p>
|
||||
Amayo Bot is intended for use by Discord users who meet Discord's minimum age requirements. We do not
|
||||
knowingly collect information from children under the age of 13. If we become aware that we have collected
|
||||
data from a child under 13, we will take steps to delete such information.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>9. International Data Transfers</h2>
|
||||
<p>
|
||||
Your data may be transferred to and processed in countries other than your own. We ensure that appropriate
|
||||
safeguards are in place to protect your data in accordance with this Privacy Policy.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>10. Cookies and Tracking</h2>
|
||||
<p>
|
||||
Our documentation website may use cookies and similar tracking technologies to enhance user experience.
|
||||
The bot itself does not use cookies, but the web dashboard (if applicable) may use:
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Essential Cookies:</strong> Required for authentication and security</li>
|
||||
<li><strong>Analytics Cookies:</strong> To understand how users interact with the website</li>
|
||||
<li><strong>Preference Cookies:</strong> To remember your settings and preferences</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>11. Changes to This Policy</h2>
|
||||
<p>
|
||||
We may update this Privacy Policy from time to time. We will notify users of significant changes through:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Announcements in our support server</li>
|
||||
<li>Updates on our documentation website</li>
|
||||
<li>Bot notifications (if applicable)</li>
|
||||
</ul>
|
||||
<p>
|
||||
Continued use of the bot after changes indicates acceptance of the updated policy.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>12. GDPR Compliance</h2>
|
||||
<p>
|
||||
For users in the European Union, we comply with the General Data Protection Regulation (GDPR). This includes:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Lawful basis for processing your data</li>
|
||||
<li>Transparent data collection and usage practices</li>
|
||||
<li>Your right to access, rectify, and delete your data</li>
|
||||
<li>Data portability</li>
|
||||
<li>The right to object to processing</li>
|
||||
<li>The right to lodge a complaint with a supervisory authority</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>13. Contact Us</h2>
|
||||
<p>
|
||||
If you have any questions, concerns, or requests regarding this Privacy Policy or your data, please contact us:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Support Server:</strong>
|
||||
<a href="https://discord.gg/your-support-server" target="_blank" rel="noopener noreferrer" class="link">
|
||||
Join our Discord
|
||||
</a>
|
||||
</li>
|
||||
<li><strong>Email:</strong> privacy@amayo.dev (if available)</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="legal-footer">
|
||||
<router-link to="/docs" class="back-btn">← Back to Documentation</router-link>
|
||||
<router-link to="/terms" class="link">Terms of Service</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue';
|
||||
import AnimatedBackground from '../components/AnimatedBackground.vue';
|
||||
import IslandNavbar from '../components/docs/IslandNavbar.vue';
|
||||
import { useTheme } from '../composables/useTheme';
|
||||
|
||||
const { initTheme } = useTheme();
|
||||
|
||||
onMounted(() => {
|
||||
initTheme();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.legal-page {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
padding: 120px 20px 60px;
|
||||
}
|
||||
|
||||
.legal-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.legal-header {
|
||||
text-align: center;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.legal-header h1 {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 16px;
|
||||
background: linear-gradient(135deg, #fff, var(--color-secondary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.legal-content {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.legal-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.legal-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.legal-section h2 {
|
||||
color: var(--color-primary);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.legal-section h3 {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 1.2rem;
|
||||
margin: 24px 0 12px;
|
||||
}
|
||||
|
||||
.legal-section p {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
line-height: 1.8;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.legal-section ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.legal-section li {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
padding: 8px 0 8px 24px;
|
||||
position: relative;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.legal-section li::before {
|
||||
content: '•';
|
||||
color: var(--color-primary);
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
.highlight-content strong {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.link {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
border-bottom-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.legal-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-decoration: none;
|
||||
padding: 12px 24px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 25px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: white;
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.legal-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.legal-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.legal-footer {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
293
AmayoWeb/src/views/TermsOfService.vue
Normal file
293
AmayoWeb/src/views/TermsOfService.vue
Normal file
@@ -0,0 +1,293 @@
|
||||
<template>
|
||||
<div class="legal-page">
|
||||
<AnimatedBackground />
|
||||
<IslandNavbar />
|
||||
|
||||
<div class="legal-container">
|
||||
<div class="legal-header">
|
||||
<h1>📜 Terms of Service</h1>
|
||||
<p class="last-updated">Last Updated: November 6, 2025</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-content">
|
||||
<section class="legal-section">
|
||||
<h2>1. Acceptance of Terms</h2>
|
||||
<p>
|
||||
By inviting Amayo Bot to your Discord server or using any of its services, you agree to be bound by these Terms of Service.
|
||||
If you do not agree to these terms, please do not use the bot.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>2. Description of Service</h2>
|
||||
<p>
|
||||
Amayo Bot is a Discord bot that provides various features including but not limited to:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Server moderation tools</li>
|
||||
<li>Alliance management system</li>
|
||||
<li>Economy and rewards system</li>
|
||||
<li>Utility commands</li>
|
||||
<li>AI-powered chat interactions</li>
|
||||
<li>Custom display components</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>3. User Responsibilities</h2>
|
||||
<p>As a user of Amayo Bot, you agree to:</p>
|
||||
<ul>
|
||||
<li>Use the bot in compliance with Discord's Terms of Service and Community Guidelines</li>
|
||||
<li>Not use the bot for any illegal or unauthorized purpose</li>
|
||||
<li>Not attempt to exploit, manipulate, or abuse the bot's features</li>
|
||||
<li>Not reverse engineer, decompile, or attempt to extract the source code of the bot</li>
|
||||
<li>Not spam, harass, or abuse other users through the bot</li>
|
||||
<li>Take full responsibility for the content you create and share using the bot</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>4. Data Collection and Usage</h2>
|
||||
<p>
|
||||
Amayo Bot collects and stores certain data to provide its services. This includes:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Discord User IDs and Server IDs</li>
|
||||
<li>Server configuration settings</li>
|
||||
<li>Alliance data and points</li>
|
||||
<li>Command usage statistics</li>
|
||||
<li>Chat messages for AI functionality (temporary storage)</li>
|
||||
</ul>
|
||||
<p>
|
||||
For more detailed information about data collection, please refer to our
|
||||
<router-link to="/privacy" class="link">Privacy Policy</router-link>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>5. Intellectual Property</h2>
|
||||
<p>
|
||||
All content, features, and functionality of Amayo Bot are owned by the bot developers and are protected by
|
||||
international copyright, trademark, and other intellectual property laws.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>6. Service Availability</h2>
|
||||
<p>
|
||||
We strive to maintain high availability of Amayo Bot, but we do not guarantee uninterrupted service.
|
||||
The bot may be temporarily unavailable due to:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Scheduled maintenance</li>
|
||||
<li>Technical issues</li>
|
||||
<li>Third-party service disruptions (Discord API)</li>
|
||||
<li>Force majeure events</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>7. Limitation of Liability</h2>
|
||||
<p>
|
||||
Amayo Bot is provided "as is" without any warranties, expressed or implied. We are not liable for:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Any damages or losses resulting from the use or inability to use the bot</li>
|
||||
<li>Data loss or corruption</li>
|
||||
<li>Actions taken by users of the bot</li>
|
||||
<li>Third-party content or services</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>8. Termination</h2>
|
||||
<p>
|
||||
We reserve the right to terminate or suspend access to Amayo Bot at any time, without prior notice, for:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Violation of these Terms of Service</li>
|
||||
<li>Abuse of the bot's features</li>
|
||||
<li>Illegal activities</li>
|
||||
<li>Any reason we deem necessary to protect the service or other users</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>9. Changes to Terms</h2>
|
||||
<p>
|
||||
We reserve the right to modify these Terms of Service at any time. Changes will be effective immediately
|
||||
upon posting. Continued use of the bot after changes constitutes acceptance of the new terms.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>10. Governing Law</h2>
|
||||
<p>
|
||||
These Terms of Service are governed by and construed in accordance with applicable international laws.
|
||||
Any disputes arising from these terms will be resolved through appropriate legal channels.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>11. Contact Information</h2>
|
||||
<p>
|
||||
If you have any questions about these Terms of Service, please contact us through our
|
||||
<a href="https://discord.gg/your-support-server" target="_blank" rel="noopener noreferrer" class="link">
|
||||
support server
|
||||
</a>.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="legal-footer">
|
||||
<router-link to="/docs" class="back-btn">← Back to Documentation</router-link>
|
||||
<router-link to="/privacy" class="link">Privacy Policy</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue';
|
||||
import AnimatedBackground from '../components/AnimatedBackground.vue';
|
||||
import IslandNavbar from '../components/docs/IslandNavbar.vue';
|
||||
import { useTheme } from '../composables/useTheme';
|
||||
|
||||
const { initTheme } = useTheme();
|
||||
|
||||
onMounted(() => {
|
||||
initTheme();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.legal-page {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
padding: 120px 20px 60px;
|
||||
}
|
||||
|
||||
.legal-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.legal-header {
|
||||
text-align: center;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.legal-header h1 {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 16px;
|
||||
background: linear-gradient(135deg, #fff, var(--color-secondary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.legal-content {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.legal-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.legal-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.legal-section h2 {
|
||||
color: var(--color-primary);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.legal-section p {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
line-height: 1.8;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.legal-section ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.legal-section li {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
padding: 8px 0 8px 24px;
|
||||
position: relative;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.legal-section li::before {
|
||||
content: '•';
|
||||
color: var(--color-primary);
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
border-bottom-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.legal-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-decoration: none;
|
||||
padding: 12px 24px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 25px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: white;
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.legal-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.legal-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.legal-footer {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user