feat: Add JWT auth and nomenclature sync to data-entry-app
Integrate shared JWT authentication into data-entry-app: - Add Oracle pool initialization for auth service - Add AuthenticationMiddleware to protect API routes - Update all receipt endpoints to use CurrentUser from JWT - Add shared auth router (/api/auth/login, /api/auth/refresh) Add nomenclature synchronization feature: - Create SQLite models for synced suppliers, local suppliers, and cash registers - Add nomenclature router with sync triggers and CRUD endpoints - Add sync service for Oracle → SQLite nomenclature data - Update nomenclature_service to use synced SQLite data with fallbacks Create shared frontend components: - Add shared/frontend/ with LoginView.vue, auth store factory, login.css - Integrate shared login and auth into data-entry-app frontend - Add axios-based API service with token refresh interceptor 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
212
shared/frontend/components/LoginView.vue
Normal file
212
shared/frontend/components/LoginView.vue
Normal file
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="login-wrapper">
|
||||
<Card class="login-card">
|
||||
<template #header>
|
||||
<div class="login-header">
|
||||
<i :class="['pi', appIcon, 'text-primary', 'text-6xl']"></i>
|
||||
<h1 class="login-title">{{ appTitle }}</h1>
|
||||
<p class="login-subtitle">{{ appSubtitle }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<form @submit.prevent="handleLogin" class="login-form">
|
||||
<div class="form-group">
|
||||
<label for="username" class="form-label required">Utilizator</label>
|
||||
<InputText
|
||||
id="username"
|
||||
v-model="credentials.username"
|
||||
placeholder="Introduceți numele de utilizator"
|
||||
:class="{ invalid: formErrors.username }"
|
||||
class="w-full"
|
||||
autocomplete="username"
|
||||
@blur="validateField('username')"
|
||||
/>
|
||||
<span v-if="formErrors.username" class="form-error">
|
||||
{{ formErrors.username }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password" class="form-label required">Parolă</label>
|
||||
<Password
|
||||
id="password"
|
||||
v-model="credentials.password"
|
||||
placeholder="Introduceți parola"
|
||||
:class="{ invalid: formErrors.password }"
|
||||
class="w-full"
|
||||
:feedback="false"
|
||||
toggle-mask
|
||||
autocomplete="current-password"
|
||||
@blur="validateField('password')"
|
||||
/>
|
||||
<span v-if="formErrors.password" class="form-error">
|
||||
{{ formErrors.password }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="authStore.error" class="login-error-message">
|
||||
<i class="pi pi-exclamation-triangle"></i>
|
||||
<span>{{ authStore.error }}</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
label="Conectare"
|
||||
class="w-full login-button"
|
||||
:loading="authStore.isLoading"
|
||||
:disabled="!isFormValid"
|
||||
/>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="login-footer">
|
||||
<small class="text-color-secondary">
|
||||
ROA2WEB © {{ currentYear }} - Toate drepturile rezervate
|
||||
</small>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useToast } from "primevue/usetoast";
|
||||
|
||||
// Props for app-specific customization
|
||||
const props = defineProps({
|
||||
appTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
appSubtitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
appIcon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
redirectPath: {
|
||||
type: String,
|
||||
default: "/",
|
||||
},
|
||||
authStore: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
|
||||
// Form data
|
||||
const credentials = ref({
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
|
||||
const formErrors = ref({
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
|
||||
// Computed properties
|
||||
const currentYear = computed(() => new Date().getFullYear());
|
||||
|
||||
const isFormValid = computed(() => {
|
||||
return (
|
||||
credentials.value.username.trim() !== "" &&
|
||||
credentials.value.password.trim() !== "" &&
|
||||
!formErrors.value.username &&
|
||||
!formErrors.value.password
|
||||
);
|
||||
});
|
||||
|
||||
// Methods
|
||||
const validateField = (field) => {
|
||||
switch (field) {
|
||||
case "username":
|
||||
formErrors.value.username =
|
||||
credentials.value.username.trim() === ""
|
||||
? "Numele de utilizator este obligatoriu"
|
||||
: "";
|
||||
break;
|
||||
case "password":
|
||||
formErrors.value.password =
|
||||
credentials.value.password.trim() === ""
|
||||
? "Parola este obligatorie"
|
||||
: "";
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
validateField("username");
|
||||
validateField("password");
|
||||
return isFormValid.value;
|
||||
};
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await props.authStore.login(credentials.value);
|
||||
|
||||
if (result.success) {
|
||||
router.push(props.redirectPath);
|
||||
} else {
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "Eroare de conectare",
|
||||
detail: result.error || "Date de conectare incorecte",
|
||||
life: 5000,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Login error:", error);
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "Eroare",
|
||||
detail: "A apărut o eroare neașteptată",
|
||||
life: 5000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Clear errors when user starts typing
|
||||
const clearErrors = () => {
|
||||
props.authStore.clearError();
|
||||
formErrors.value = {
|
||||
username: "",
|
||||
password: "",
|
||||
};
|
||||
};
|
||||
|
||||
// Lifecycle hooks
|
||||
onMounted(() => {
|
||||
// Clear any previous errors
|
||||
clearErrors();
|
||||
|
||||
// Focus on username field
|
||||
const usernameInput = document.getElementById("username");
|
||||
if (usernameInput) {
|
||||
usernameInput.focus();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
clearErrors();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import "../styles/login.css";
|
||||
</style>
|
||||
Reference in New Issue
Block a user