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,139 @@
import axios from "axios";
// Create axios instance with base configuration
const apiService = axios.create({
baseURL: import.meta.env.BASE_URL + "api",
timeout: 10000,
headers: {
"Content-Type": "application/json",
},
});
// Request interceptor to add auth token
apiService.interceptors.request.use(
(config) => {
const token = localStorage.getItem("access_token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
},
);
// Response interceptor for handling errors and token refresh
apiService.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const originalRequest = error.config;
// Handle 401 Unauthorized errors
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const refreshToken = localStorage.getItem("refresh_token");
if (refreshToken) {
const response = await axios.post(import.meta.env.BASE_URL + "api/auth/refresh", {
refresh_token: refreshToken,
});
const { access_token } = response.data;
localStorage.setItem("access_token", access_token);
// Update the authorization header
apiService.defaults.headers.common["Authorization"] =
`Bearer ${access_token}`;
originalRequest.headers["Authorization"] = `Bearer ${access_token}`;
// Retry the original request
return apiService(originalRequest);
}
} catch (refreshError) {
// Refresh failed, clear tokens and redirect to login
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
localStorage.removeItem("user");
// Note: selected_company is now per-user (selected_company_${username})
// and persists across sessions - not cleared on token expiry
// Redirect to login page
const loginPath = import.meta.env.BASE_URL + "login";
if (window.location.pathname !== loginPath) {
window.location.href = loginPath;
}
return Promise.reject(refreshError);
}
}
// Handle other errors
if (error.response) {
// Server responded with error status
const message =
error.response.data?.detail ||
error.response.data?.message ||
`Server error: ${error.response.status}`;
console.error("API Error:", {
status: error.response.status,
message: message,
url: error.config.url,
});
} else if (error.request) {
// Request was made but no response received
console.error("Network Error:", error.message);
} else {
// Something else happened
console.error("Request Error:", error.message);
}
return Promise.reject(error);
},
);
// API service methods
export { apiService };
// Specific API endpoints
export const authAPI = {
login: (credentials) => {
return apiService.post("/auth/login", {
username: credentials.username,
password: credentials.password,
});
},
refresh: (refreshToken) => {
return apiService.post("/auth/refresh", {
refresh_token: refreshToken,
});
},
logout: () => {
return apiService.post("/auth/logout");
},
};
export const companiesAPI = {
getAll: () => {
return apiService.get("/companies");
},
};
export const invoicesAPI = {
getByCompany: (companyCode, params = {}) => {
return apiService.get(`/invoices/${companyCode}`, { params });
},
getById: (companyCode, invoiceId) => {
return apiService.get(`/invoices/${companyCode}/${invoiceId}`);
},
};
export default apiService;

View File

@@ -0,0 +1,6 @@
export {
apiService,
authAPI,
companiesAPI,
invoicesAPI,
} from "./api";