Fix .gitignore and add missing authentication source files
This commit fixes overly broad .gitignore patterns that were excluding important source code files from version control. Previously, wildcard patterns like *auth*, *token*, *secret*, *connection*, and *credential* were excluding ALL files containing these words, including critical application code. Changes: - Updated .gitignore with specific patterns for sensitive config files (*.json, *.txt, *.yml, *.yaml extensions only) - Removed broad wildcards that excluded source code files Added missing source files: - shared/auth/ (9 files): Complete authentication system - JWT handler, middleware, auth service, models, routes - reports-app/backend/app/routers/auth.py: Authentication API router - reports-app/backend/app/auth_middleware_wrapper.py: Middleware wrapper - reports-app/frontend/src/stores/auth.js: Vue.js auth store - reports-app/frontend/tests/: E2E tests and fixtures for auth - reports-app/telegram-bot/app/auth/: Telegram auth linking module - deployment/windows/scripts/Setup-ClaudeAuth.ps1: Windows deployment script - security/secrets_scanner.py: Security scanning utility These files are essential for the application to function and should have been included in the initial commit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
116
reports-app/frontend/src/stores/auth.js
Normal file
116
reports-app/frontend/src/stores/auth.js
Normal file
@@ -0,0 +1,116 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, computed } from "vue";
|
||||
import { apiService } from "../services/api";
|
||||
|
||||
export const useAuthStore = defineStore("auth", () => {
|
||||
// State
|
||||
const accessToken = ref(localStorage.getItem("access_token"));
|
||||
const refreshToken = ref(localStorage.getItem("refresh_token"));
|
||||
const user = ref(JSON.parse(localStorage.getItem("user") || "null"));
|
||||
const isLoading = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
// Getters
|
||||
const isAuthenticated = computed(() => !!accessToken.value);
|
||||
const currentUser = computed(() => user.value);
|
||||
|
||||
// Actions
|
||||
const login = async (credentials) => {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const loginData = {
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
};
|
||||
|
||||
const response = await apiService.post("/auth/login", loginData);
|
||||
const { access_token, refresh_token, user: userData } = response.data;
|
||||
|
||||
accessToken.value = access_token;
|
||||
refreshToken.value = refresh_token;
|
||||
user.value = userData;
|
||||
|
||||
localStorage.setItem("access_token", access_token);
|
||||
localStorage.setItem("refresh_token", refresh_token);
|
||||
localStorage.setItem("user", JSON.stringify(userData));
|
||||
|
||||
apiService.defaults.headers.common["Authorization"] = `Bearer ${access_token}`;
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.detail || "Login failed";
|
||||
return { success: false, error: error.value };
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
accessToken.value = null;
|
||||
refreshToken.value = null;
|
||||
user.value = null;
|
||||
error.value = null;
|
||||
|
||||
localStorage.removeItem("access_token");
|
||||
localStorage.removeItem("refresh_token");
|
||||
localStorage.removeItem("user");
|
||||
// Note: selected_company is now per-user and persists across logout/login
|
||||
// It's stored as 'selected_company_${username}' in localStorage
|
||||
|
||||
delete apiService.defaults.headers.common["Authorization"];
|
||||
};
|
||||
|
||||
const refreshAccessToken = async () => {
|
||||
if (!refreshToken.value) {
|
||||
logout();
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiService.post("/auth/refresh", {
|
||||
refresh_token: refreshToken.value,
|
||||
});
|
||||
|
||||
const { access_token } = response.data;
|
||||
|
||||
accessToken.value = access_token;
|
||||
localStorage.setItem("access_token", access_token);
|
||||
apiService.defaults.headers.common["Authorization"] = `Bearer ${access_token}`;
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error("Token refresh failed:", err);
|
||||
logout();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const initializeAuth = () => {
|
||||
if (accessToken.value) {
|
||||
apiService.defaults.headers.common["Authorization"] = `Bearer ${accessToken.value}`;
|
||||
}
|
||||
};
|
||||
|
||||
const clearError = () => {
|
||||
error.value = null;
|
||||
};
|
||||
|
||||
initializeAuth();
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
user,
|
||||
isLoading,
|
||||
error,
|
||||
isAuthenticated,
|
||||
currentUser,
|
||||
login,
|
||||
logout,
|
||||
refreshAccessToken,
|
||||
initializeAuth,
|
||||
clearError,
|
||||
};
|
||||
});
|
||||
223
reports-app/frontend/tests/e2e/auth/login.spec.js
Normal file
223
reports-app/frontend/tests/e2e/auth/login.spec.js
Normal file
@@ -0,0 +1,223 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from '../../page-objects/LoginPage.js';
|
||||
import { testCredentials } from '../../fixtures/auth.js';
|
||||
|
||||
test.describe('Authentication - Login Flow', () => {
|
||||
let loginPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
await loginPage.navigate();
|
||||
});
|
||||
|
||||
test('should display login page correctly', async ({ page }) => {
|
||||
// Check page title and main elements
|
||||
await expect(page).toHaveTitle(/ROA Reports/);
|
||||
|
||||
// Check login form elements are visible
|
||||
await expect(page.locator(loginPage.usernameInput)).toBeVisible();
|
||||
await expect(page.locator(loginPage.passwordInput)).toBeVisible();
|
||||
await expect(page.locator(loginPage.loginButton)).toBeVisible();
|
||||
|
||||
// Check page title
|
||||
const title = await loginPage.getPageTitle();
|
||||
expect(title).toContain('ROA Reports');
|
||||
});
|
||||
|
||||
test('should show validation errors for empty fields', async ({ page }) => {
|
||||
// Check initial state - button should be disabled
|
||||
expect(await loginPage.isLoginButtonDisabled()).toBe(true);
|
||||
|
||||
// Clear any existing content and verify empty state
|
||||
await page.fill(loginPage.usernameInput, '');
|
||||
await page.fill(loginPage.passwordInput, '');
|
||||
await page.click(loginPage.loginCard); // Click outside to trigger validation
|
||||
|
||||
// Wait for Vue reactivity
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Button should remain disabled with empty fields
|
||||
expect(await loginPage.isLoginButtonDisabled()).toBe(true);
|
||||
|
||||
// Verify form validation classes are applied
|
||||
const hasInvalidFields = await loginPage.hasInvalidField();
|
||||
// Note: validation might not show invalid state until user interaction
|
||||
});
|
||||
|
||||
test('should show validation error for empty username', async ({ page }) => {
|
||||
// Fill only password
|
||||
await loginPage.fillCredentials('', 'password123');
|
||||
|
||||
// Trigger validation by clicking outside
|
||||
await page.click(loginPage.loginCard);
|
||||
|
||||
// Check that login button is disabled
|
||||
expect(await loginPage.isLoginButtonDisabled()).toBe(true);
|
||||
});
|
||||
|
||||
test('should show validation error for empty password', async ({ page }) => {
|
||||
// Fill only username
|
||||
await loginPage.fillCredentials('username', '');
|
||||
|
||||
// Trigger validation by clicking outside
|
||||
await page.click(loginPage.loginCard);
|
||||
|
||||
// Check that login button is disabled
|
||||
expect(await loginPage.isLoginButtonDisabled()).toBe(true);
|
||||
});
|
||||
|
||||
test('should enable login button with valid input', async ({ page: _page }) => {
|
||||
// Fill both fields
|
||||
await loginPage.fillCredentials('testuser', 'testpass');
|
||||
|
||||
// Check that login button is enabled
|
||||
expect(await loginPage.isLoginButtonDisabled()).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle invalid credentials', async ({ page }) => {
|
||||
// Mock the API response for invalid login
|
||||
await page.route('**/api/auth/login', async route => {
|
||||
await route.fulfill({
|
||||
status: 401,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
detail: 'Invalid credentials'
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Attempt login with invalid credentials
|
||||
await loginPage.login(testCredentials.invalid.username, testCredentials.invalid.password);
|
||||
|
||||
// Wait for response
|
||||
await loginPage.waitForLoginResult();
|
||||
|
||||
// Check that we're still on login page
|
||||
expect(await loginPage.isOnLoginPage()).toBe(true);
|
||||
|
||||
// Check that error message appears (via toast or error div)
|
||||
// Note: Error might be shown via PrimeVue Toast, so we need to check for toast messages
|
||||
const toastError = page.locator('.p-toast-message-error');
|
||||
if (await toastError.isVisible()) {
|
||||
const errorText = await toastError.textContent();
|
||||
expect(errorText).toContain('Eroare');
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle successful login', async ({ page }) => {
|
||||
// Mock the API response for successful login
|
||||
await page.route('**/api/auth/login', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
access_token: 'mock_access_token',
|
||||
refresh_token: 'mock_refresh_token',
|
||||
user: {
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
full_name: 'Test User'
|
||||
}
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Mock companies endpoint for dashboard
|
||||
await page.route('**/api/companies', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([
|
||||
{ code: 'COMP1', name: 'Company 1' },
|
||||
{ code: 'COMP2', name: 'Company 2' }
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
// Attempt login with valid credentials
|
||||
await loginPage.login(testCredentials.valid.username, testCredentials.valid.password);
|
||||
|
||||
// Wait for redirect to dashboard
|
||||
await page.waitForURL('/dashboard', { timeout: 10000 });
|
||||
|
||||
// Check that we're on dashboard page
|
||||
expect(page.url()).toContain('/dashboard');
|
||||
});
|
||||
|
||||
test('should show loading state during login', async ({ page }) => {
|
||||
// Mock slow API response
|
||||
await page.route('**/api/auth/login', async route => {
|
||||
// Delay the response to see loading state
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
access_token: 'mock_access_token',
|
||||
refresh_token: 'mock_refresh_token',
|
||||
user: { id: 1, username: 'testuser' }
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Fill credentials and submit
|
||||
await loginPage.fillCredentials(testCredentials.valid.username, testCredentials.valid.password);
|
||||
await loginPage.clickLogin();
|
||||
|
||||
// Check loading state appears
|
||||
await expect(page.locator(loginPage.loadingSpinner)).toBeVisible();
|
||||
|
||||
// Wait for loading to finish
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('should handle network errors gracefully', async ({ page }) => {
|
||||
// Mock network error
|
||||
await page.route('**/api/auth/login', async route => {
|
||||
await route.abort('failed');
|
||||
});
|
||||
|
||||
// Attempt login
|
||||
await loginPage.login(testCredentials.valid.username, testCredentials.valid.password);
|
||||
|
||||
// Wait a bit for error handling
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Should still be on login page
|
||||
expect(await loginPage.isOnLoginPage()).toBe(true);
|
||||
|
||||
// Check for error message in toast summary or detail
|
||||
const toastSummary = page.locator('.p-toast-summary');
|
||||
const toastDetail = page.locator('.p-toast-detail');
|
||||
|
||||
// Check if either summary or detail contains error text
|
||||
const summaryVisible = await toastSummary.isVisible();
|
||||
const detailVisible = await toastDetail.isVisible();
|
||||
|
||||
if (summaryVisible || detailVisible) {
|
||||
let errorFound = false;
|
||||
|
||||
if (summaryVisible) {
|
||||
const summaryText = await toastSummary.textContent();
|
||||
if (summaryText && summaryText.toLowerCase().includes('eroare')) {
|
||||
errorFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (detailVisible) {
|
||||
const detailText = await toastDetail.textContent();
|
||||
if (detailText && detailText.toLowerCase().includes('eroare')) {
|
||||
errorFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
expect(errorFound).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('should focus username field on page load', async ({ page }) => {
|
||||
// Check that username field is focused
|
||||
const focusedElement = await page.locator(':focus');
|
||||
await expect(focusedElement).toHaveAttribute('id', 'username');
|
||||
});
|
||||
});
|
||||
60
reports-app/frontend/tests/fixtures/auth.js
vendored
Normal file
60
reports-app/frontend/tests/fixtures/auth.js
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
export const testCredentials = {
|
||||
valid: {
|
||||
username: 'testuser',
|
||||
password: 'testpass123'
|
||||
},
|
||||
invalid: {
|
||||
username: 'wronguser',
|
||||
password: 'wrongpass'
|
||||
},
|
||||
empty: {
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
partialValid: {
|
||||
username: 'testuser',
|
||||
password: ''
|
||||
}
|
||||
};
|
||||
|
||||
export const expectedMessages = {
|
||||
loginSuccess: 'Conectare reușită',
|
||||
loginError: 'Eroare de conectare',
|
||||
usernameRequired: 'Numele de utilizator este obligatoriu',
|
||||
passwordRequired: 'Parola este obligatorie',
|
||||
invalidCredentials: 'Date de conectare incorecte'
|
||||
};
|
||||
|
||||
export const apiEndpoints = {
|
||||
login: '/api/auth/login',
|
||||
logout: '/api/auth/logout',
|
||||
refresh: '/api/auth/refresh',
|
||||
user: '/api/auth/user'
|
||||
};
|
||||
|
||||
export const mockApiResponses = {
|
||||
loginSuccess: {
|
||||
status: 200,
|
||||
body: {
|
||||
access_token: 'mock_access_token',
|
||||
refresh_token: 'mock_refresh_token',
|
||||
user: {
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
full_name: 'Test User'
|
||||
}
|
||||
}
|
||||
},
|
||||
loginError: {
|
||||
status: 401,
|
||||
body: {
|
||||
detail: 'Invalid credentials'
|
||||
}
|
||||
},
|
||||
unauthorized: {
|
||||
status: 401,
|
||||
body: {
|
||||
detail: 'Not authenticated'
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user