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>
213 lines
5.1 KiB
Vue
213 lines
5.1 KiB
Vue
<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>
|