Initial commit: ROA2WEB - FastAPI + Vue.js + Telegram Bot

Modern ERP Reports Application with microservices architecture

Tech Stack:
- Backend: FastAPI + python-oracledb (Oracle DB integration)
- Frontend: Vue.js 3 + PrimeVue + Vite
- Telegram Bot: python-telegram-bot + SQLite
- Infrastructure: Shared database pool, JWT authentication, SSH tunnel

Features:
- FastAPI backend with async Oracle connection pool
- Vue.js 3 responsive frontend with PrimeVue components
- Telegram bot alternative interface
- Microservices architecture with shared components
- Complete deployment support (Linux Docker + Windows IIS)
- Comprehensive testing (Playwright E2E + pytest)

Repository Structure:
- reports-app/ - Main application (backend, frontend, telegram-bot)
- shared/ - Shared components (database pool, auth, utils)
- deployment/ - Deployment scripts (Linux & Windows)
- docs/ - Project documentation
- security/ - Security scanning and git hooks
This commit is contained in:
2025-10-25 14:55:08 +03:00
commit 6b13ffa183
237 changed files with 70035 additions and 0 deletions

View File

@@ -0,0 +1,432 @@
<template>
<div>
<!-- Dashboard Header -->
<DashboardHeader @menu-toggle="handleMenuToggle" />
<!-- Hamburger Menu -->
<HamburgerMenu :is-open="menuOpen" @close="handleMenuClose" />
<!-- Main Content -->
<main class="main-content">
<div class="app-container">
<!-- Page Header -->
<div class="page-header">
<h1 class="page-title">Telegram Bot</h1>
<p class="page-subtitle">Conectează-ți contul pentru acces rapid din Telegram</p>
</div>
<!-- Loading State -->
<div v-if="loading" class="loading-state">
<div class="loading-spinner"></div>
<p>Se generează codul...</p>
</div>
<!-- Main Card -->
<div v-else class="telegram-card">
<!-- Generate Button -->
<div class="generate-section">
<button
@click="generateCode"
:disabled="loading"
class="generate-btn"
>
{{ loading ? 'Se generează...' : 'Generează Cod' }}
</button>
</div>
<!-- Code Display & Actions -->
<div v-if="linkingCode" class="code-section">
<!-- Code Display -->
<div class="code-display">
<div class="code-header">
<span class="code-label">Cod</span>
<span class="code-timer">{{ formatTime(timeRemaining) }}</span>
</div>
<div class="code-value">{{ linkingCode }}</div>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<a
:href="telegramDeepLink"
target="_blank"
rel="noopener noreferrer"
class="action-btn primary-action-btn"
>
Deschide Telegram
</a>
<Button
:label="showQR ? 'Ascunde QR' : 'Arată QR'"
@click="showQR = !showQR"
class="action-btn"
outlined
/>
<Button
label="Copiază Cod"
@click="copyCode"
class="action-btn"
outlined
/>
</div>
<!-- QR Code Display -->
<div v-if="showQR" class="qr-section">
<QRCodeVue :value="telegramDeepLink" :size="200" level="H" />
</div>
</div>
</div>
</div>
</main>
<!-- Toast -->
<Toast position="top-right" />
</div>
</template>
<script setup>
import { ref, computed, onUnmounted } from 'vue'
import { useToast } from 'primevue/usetoast'
import DashboardHeader from '../components/layout/DashboardHeader.vue'
import HamburgerMenu from '../components/layout/HamburgerMenu.vue'
import Button from 'primevue/button'
import Toast from 'primevue/toast'
import QRCodeVue from 'qrcode.vue'
import { apiService } from '../services/api'
const toast = useToast()
// State
const menuOpen = ref(false)
const linkingCode = ref('')
const timeRemaining = ref(0)
const loading = ref(false)
const showQR = ref(false)
let countdownInterval = null
// Config
const BOT_USERNAME = import.meta.env.VITE_TELEGRAM_BOT_USERNAME || 'roa2web_bot'
// Computed
const telegramDeepLink = computed(() => {
if (!linkingCode.value) return ''
return `https://t.me/${BOT_USERNAME}?start=${linkingCode.value}`
})
// Methods
const handleMenuToggle = (isOpen) => {
menuOpen.value = isOpen
}
const handleMenuClose = () => {
menuOpen.value = false
}
const generateCode = async () => {
loading.value = true
showQR.value = false
try {
const response = await apiService.post('/telegram/auth/generate-code')
linkingCode.value = response.data.linking_code
timeRemaining.value = response.data.expires_in_minutes * 60
toast.add({
severity: 'success',
summary: 'Cod Generat',
detail: 'Alege o metodă de conectare',
life: 3000
})
startCountdown()
} catch (error) {
console.error('Error generating code:', error)
toast.add({
severity: 'error',
summary: 'Eroare',
detail: error.response?.data?.detail || 'Nu am putut genera codul',
life: 5000
})
} finally {
loading.value = false
}
}
const startCountdown = () => {
if (countdownInterval) clearInterval(countdownInterval)
countdownInterval = setInterval(() => {
if (timeRemaining.value > 0) {
timeRemaining.value--
} else {
clearInterval(countdownInterval)
linkingCode.value = ''
toast.add({
severity: 'warn',
summary: 'Cod Expirat',
detail: 'Generează un cod nou',
life: 4000
})
}
}, 1000)
}
const formatTime = (seconds) => {
const minutes = Math.floor(seconds / 60)
const secs = seconds % 60
return `${minutes}:${secs.toString().padStart(2, '0')}`
}
const copyCode = async () => {
try {
await navigator.clipboard.writeText(linkingCode.value)
toast.add({
severity: 'success',
summary: 'Copiat',
detail: 'Cod copiat în clipboard',
life: 2000
})
} catch (error) {
const tempInput = document.createElement('input')
tempInput.value = linkingCode.value
document.body.appendChild(tempInput)
tempInput.select()
document.execCommand('copy')
document.body.removeChild(tempInput)
toast.add({
severity: 'success',
summary: 'Copiat',
life: 2000
})
}
}
onUnmounted(() => {
if (countdownInterval) clearInterval(countdownInterval)
})
</script>
<style scoped>
/* Layout - Consistent with Dashboard */
.main-content {
margin-left: 0;
padding: var(--space-lg);
min-height: 100vh;
background: var(--color-bg);
}
.app-container {
max-width: 1000px;
margin: 0 auto;
}
/* Page Header */
.page-header {
margin-bottom: var(--space-xl);
}
.page-title {
margin: 0 0 var(--space-xs) 0;
font-size: var(--text-2xl);
font-weight: var(--font-semibold);
color: var(--color-text);
}
.page-subtitle {
margin: 0;
font-size: var(--text-base);
color: var(--color-text-secondary);
}
/* Loading */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-md);
padding: var(--space-3xl);
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid var(--color-border);
border-top-color: var(--color-primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Main Card */
.telegram-card {
background: var(--color-bg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
padding: var(--space-xl);
border: 1px solid var(--color-border);
}
/* Generate Section */
.generate-section {
display: flex;
justify-content: center;
padding-bottom: var(--space-lg);
border-bottom: 1px solid var(--color-border);
}
.generate-btn {
min-width: 200px;
padding: 12px 24px;
background: #6366f1;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.generate-btn:hover:not(:disabled) {
background: #4f46e5;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
}
.generate-btn:active:not(:disabled) {
transform: translateY(0);
}
.generate-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Code Section */
.code-section {
margin-top: var(--space-lg);
display: flex;
flex-direction: column;
gap: var(--space-lg);
}
/* Code Display */
.code-display {
background: linear-gradient(135deg, rgba(67, 97, 238, 0.08), rgba(67, 97, 238, 0.02));
border: 2px solid var(--color-primary);
border-radius: var(--radius-md);
padding: var(--space-md);
text-align: center;
}
.code-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-xs);
font-size: var(--text-sm);
}
.code-label {
color: var(--color-text-secondary);
font-weight: var(--font-semibold);
}
.code-timer {
color: var(--color-primary);
font-weight: var(--font-bold);
font-family: 'Courier New', monospace;
}
.code-value {
font-size: 2rem;
font-weight: var(--font-bold);
color: var(--color-primary);
letter-spacing: 0.3em;
font-family: 'Courier New', monospace;
}
/* Action Buttons */
.action-buttons {
display: flex;
gap: var(--space-sm);
flex-wrap: wrap;
}
.action-btn {
flex: 1;
min-width: 160px;
justify-content: center;
}
.primary-action-btn {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
min-width: 160px;
padding: 11px 20px;
background: var(--primary-500, #6366f1);
color: white;
text-decoration: none;
border-radius: 6px;
font-weight: 600;
font-size: 14px;
transition: all 0.2s ease;
border: none;
cursor: pointer;
line-height: 1.5;
}
.primary-action-btn:hover {
background: var(--primary-600, #4f46e5);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
}
/* QR Section */
.qr-section {
display: flex;
justify-content: center;
padding: var(--space-lg);
background: white;
border-radius: var(--radius-md);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
/* Responsive */
@media (max-width: 768px) {
.main-content {
padding: var(--space-md);
}
.telegram-card {
padding: var(--space-md);
}
.code-value {
font-size: 1.5rem;
letter-spacing: 0.2em;
}
.action-buttons {
flex-direction: column;
}
.action-btn,
.primary-btn {
width: 100%;
min-width: unset;
}
}
</style>