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:
Shni
2025-11-06 23:44:44 -06:00
parent b25885d87f
commit 781f4398a4
36 changed files with 7830 additions and 57 deletions

View 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"
]
}
}

View File

@@ -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>

View File

@@ -55,7 +55,6 @@
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {

View File

@@ -26,10 +26,4 @@ a,
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}

View File

@@ -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;
}

View File

@@ -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%;

View 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>

View 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>

View File

@@ -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',

View File

@@ -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')
}
]
})

View File

@@ -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

View File

@@ -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
}
},

View 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();

View 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>

View 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>

View 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>

View 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>