fixed response data for login,

fixed tabs
This commit is contained in:
2026-02-13 22:01:40 +01:00
parent ae7f05786d
commit 210ab43a0b
2 changed files with 152 additions and 157 deletions

View File

@ -25,6 +25,8 @@ It describes what the project is, what is already implemented, and what still ne
- `frontend/src/router/index.ts` maps `/` to `frontend/src/views/AuthView.vue`. - `frontend/src/router/index.ts` maps `/` to `frontend/src/views/AuthView.vue`.
- `AuthView` serves as login + registration entry (single form, email + password). - `AuthView` serves as login + registration entry (single form, email + password).
- Successful login stores `token` and `user_email` in `localStorage`. - Successful login stores `token` and `user_email` in `localStorage`.
- `frontend/src/views/AuthView.vue` formatting now uses tab-based indentation.
- Login response parsing in `AuthView` now reads user fields from `response.data.user`.
- Frontend i18n is wired: - Frontend i18n is wired:
- setup in `frontend/src/i18n/index.ts` - setup in `frontend/src/i18n/index.ts`
- locale files in `frontend/src/locales/{sk,cs,en,es,de}.ts` - locale files in `frontend/src/locales/{sk,cs,en,es,de}.ts`
@ -34,8 +36,7 @@ It describes what the project is, what is already implemented, and what still ne
- design tokens in `frontend/src/assets/css/style.css` (`:root` variables). - design tokens in `frontend/src/assets/css/style.css` (`:root` variables).
- App logo is served from `frontend/public/Nutrio.png` (copied from `doc/Nutrio.png`). - App logo is served from `frontend/public/Nutrio.png` (copied from `doc/Nutrio.png`).
- Font Awesome is installed and registered globally in `frontend/src/main.ts`. - Font Awesome is installed and registered globally in `frontend/src/main.ts`.
- `frontend/src/BackendAPI.js` is generated via `backend/scripts/buildTypeScript.php` and should not be edited manually. - `frontend/src/BackendAPI.ts` is generated via `backend/scripts/buildTypeScript.php` and should not be edited manually.
- `frontend/src/BackendAPI.d.ts` provides TS declarations for generated `BackendAPI.js`.
- `backend/data.json` contains sample meal data (not currently wired into DB/API flow). - `backend/data.json` contains sample meal data (not currently wired into DB/API flow).
## Backend Architecture ## Backend Architecture
@ -139,8 +140,11 @@ All actions are invoked through `backend/public/API.php` with `?action=<method_n
- Some comments in `Maintenance.php` show encoding artifacts, but SQL structure is valid. - Some comments in `Maintenance.php` show encoding artifacts, but SQL structure is valid.
- Basic token auth is implemented, but token is still passed as plain API parameter. - Basic token auth is implemented, but token is still passed as plain API parameter.
- For `array` parameters (for example `ordered_item_ids`), APIlite expects JSON in request payload. - For `array` parameters (for example `ordered_item_ids`), APIlite expects JSON in request payload.
- APIlite wraps responses with a nested `data` object. Keep this in mind on frontend parsing. - APIlite response handling detail:
- `frontend/src/BackendAPI.js` is generated output; regenerate when backend API changes, do not patch manually. - raw API response is wrapped as `{ status, data }`
- generated `BackendAPI.ts` currently resolves `response.data` in `callPromise` for non-`__HELP__` actions
- frontend parsing must match the actual returned runtime shape
- `frontend/src/BackendAPI.ts` is generated output; regenerate when backend API changes, do not patch manually.
- In vue-i18n locale strings, `@` must be escaped as `{'@'}` to avoid "Invalid linked format" errors. - In vue-i18n locale strings, `@` must be escaped as `{'@'}` to avoid "Invalid linked format" errors.
## Local Runbook ## Local Runbook
@ -176,3 +180,4 @@ Frontend:
- Keep MySQL + SQLite compatibility in SQL where possible (project supports both). - Keep MySQL + SQLite compatibility in SQL where possible (project supports both).
- When changing schema, always bump DB version in `Maintenance.php` with forward-only migration steps. - When changing schema, always bump DB version in `Maintenance.php` with forward-only migration steps.
- Keep API action names stable unless frontend is updated at the same time. - Keep API action names stable unless frontend is updated at the same time.
- In source files, use tab characters for indentation (do not add space-based indentation).

View File

@ -2,14 +2,14 @@
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { import {
faEnvelope, faEnvelope,
faEye, faEye,
faEyeSlash, faEyeSlash,
faGlobe, faGlobe,
faLock, faLock,
faMoon, faMoon,
faRightToBracket, faRightToBracket,
faSun, faSun,
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import BackendAPI from '@/BackendAPI.ts' import BackendAPI from '@/BackendAPI.ts'
@ -19,11 +19,13 @@ import { SUPPORTED_LOCALES, type AppLocale } from '@/i18n'
type ThemeMode = 'light' | 'dark' type ThemeMode = 'light' | 'dark'
type LoginResponse = { type LoginResponse = {
auto_registered?: boolean data?: {
user?: { auto_registered?: boolean
email?: string | null user?: {
token?: string | null email?: string | null
} token?: string | null
}
}
} }
const { t } = useI18n({ useScope: 'global' }) const { t } = useI18n({ useScope: 'global' })
@ -36,199 +38,187 @@ const errorMessage = ref('')
const successMessage = ref('') const successMessage = ref('')
const isSupportedLocale = (value: string): value is AppLocale => { const isSupportedLocale = (value: string): value is AppLocale => {
return SUPPORTED_LOCALES.includes(value as AppLocale) return SUPPORTED_LOCALES.includes(value as AppLocale)
} }
const setLocale = (value: string) => { const setLocale = (value: string) => {
if (!isSupportedLocale(value)) { if (!isSupportedLocale(value)) {
return return
} }
i18n.global.locale.value = value i18n.global.locale.value = value
document.documentElement.setAttribute('lang', value) document.documentElement.setAttribute('lang', value)
localStorage.setItem('locale', value) localStorage.setItem('locale', value)
} }
const localeValue = computed({ const localeValue = computed({
get: () => i18n.global.locale.value as AppLocale, get: () => i18n.global.locale.value as AppLocale,
set: (value: string) => setLocale(value), set: (value: string) => setLocale(value),
}) })
watch( watch(
() => i18n.global.locale.value, () => i18n.global.locale.value,
(nextLocale) => { (nextLocale) => {
document.documentElement.setAttribute('lang', nextLocale) document.documentElement.setAttribute('lang', nextLocale)
}, },
{ immediate: true }, { immediate: true },
) )
const getInitialTheme = (): ThemeMode => { const getInitialTheme = (): ThemeMode => {
const storedTheme = localStorage.getItem('theme') const storedTheme = localStorage.getItem('theme')
return storedTheme === 'dark' ? 'dark' : 'light' return storedTheme === 'dark' ? 'dark' : 'light'
} }
const theme = ref<ThemeMode>(getInitialTheme()) const theme = ref<ThemeMode>(getInitialTheme())
const applyTheme = (nextTheme: ThemeMode) => { const applyTheme = (nextTheme: ThemeMode) => {
theme.value = nextTheme theme.value = nextTheme
document.documentElement.setAttribute('data-theme', nextTheme) document.documentElement.setAttribute('data-theme', nextTheme)
localStorage.setItem('theme', nextTheme) localStorage.setItem('theme', nextTheme)
} }
applyTheme(theme.value) applyTheme(theme.value)
const toggleTheme = () => { const toggleTheme = () => {
applyTheme(theme.value === 'dark' ? 'light' : 'dark') applyTheme(theme.value === 'dark' ? 'light' : 'dark')
} }
const isDarkMode = computed(() => theme.value === 'dark') const isDarkMode = computed(() => theme.value === 'dark')
const themeLabel = computed(() => { const themeLabel = computed(() => {
return isDarkMode.value ? t('theme.dark') : t('theme.light') return isDarkMode.value ? t('theme.dark') : t('theme.light')
}) })
const submitLabel = computed(() => { const submitLabel = computed(() => {
return isLoading.value ? t('auth.submitting') : t('auth.submit') return isLoading.value ? t('auth.submitting') : t('auth.submit')
}) })
const mapApiError = (error: unknown): string => { const mapApiError = (error: unknown): string => {
if (typeof error !== 'string') { if (typeof error !== 'string') {
return t('auth.errors.loginFailed') return t('auth.errors.loginFailed')
} }
if (error === 'Invalid email or password') { if (error === 'Invalid email or password') {
return t('auth.errors.invalidCredentials') return t('auth.errors.invalidCredentials')
} }
if (error === 'Invalid email format') { if (error === 'Invalid email format') {
return t('auth.errors.invalidEmail') return t('auth.errors.invalidEmail')
} }
return t('auth.errors.loginFailed') return t('auth.errors.loginFailed')
} }
const submitForm = async () => { const submitForm = async () => {
errorMessage.value = '' errorMessage.value = ''
successMessage.value = '' successMessage.value = ''
const normalizedEmail = email.value.trim().toLowerCase() const normalizedEmail = email.value.trim().toLowerCase()
const normalizedPassword = password.value const normalizedPassword = password.value
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(normalizedEmail)) { if (!emailRegex.test(normalizedEmail)) {
errorMessage.value = t('auth.errors.invalidEmail') errorMessage.value = t('auth.errors.invalidEmail')
return return
} }
if (normalizedPassword.length <= 0) { if (normalizedPassword.length <= 0) {
errorMessage.value = t('auth.errors.passwordRequired') errorMessage.value = t('auth.errors.passwordRequired')
return return
} }
isLoading.value = true isLoading.value = true
try { try {
const response = (await BackendAPI.userLogin(normalizedEmail, normalizedPassword)) as LoginResponse const response = (await BackendAPI.userLogin(
const token = response.user?.token ?? null normalizedEmail,
const userEmail = response.user?.email ?? normalizedEmail normalizedPassword,
)) as LoginResponse
const token = response.data?.user?.token ?? null
const userEmail = response.data?.user?.email ?? normalizedEmail
console.log(response)
if (token) {
localStorage.setItem('token', token)
} else {
localStorage.removeItem('token')
}
localStorage.setItem('user_email', userEmail)
if (token) { successMessage.value = response.auto_registered
localStorage.setItem('token', token) ? t('auth.successAutoRegistered')
} else { : t('auth.successLoggedIn')
localStorage.removeItem('token')
}
localStorage.setItem('user_email', userEmail)
successMessage.value = response.auto_registered email.value = normalizedEmail
? t('auth.successAutoRegistered') password.value = ''
: t('auth.successLoggedIn') showPassword.value = false
} catch (error) {
email.value = normalizedEmail errorMessage.value = mapApiError(error)
password.value = '' } finally {
showPassword.value = false isLoading.value = false
} catch (error) { }
errorMessage.value = mapApiError(error)
} finally {
isLoading.value = false
}
} }
</script> </script>
<template> <template>
<main class="auth-page"> <main class="auth-page">
<div class="auth-shell"> <div class="auth-shell">
<section class="auth-brand"> <section class="auth-brand">
<img src="/Nutrio.png" :alt="t('app.name')" class="auth-logo" /> <img src="/Nutrio.png" :alt="t('app.name')" class="auth-logo" />
<h1>{{ t('app.name') }}</h1> <h1>{{ t('app.name') }}</h1>
<p>{{ t('app.slogan') }}</p> <p>{{ t('app.slogan') }}</p>
</section> </section>
<section class="auth-card"> <section class="auth-card">
<div class="auth-toolbar"> <div class="auth-toolbar">
<label class="locale-control" :aria-label="t('auth.languageLabel')"> <label class="locale-control" :aria-label="t('auth.languageLabel')">
<font-awesome-icon :icon="faGlobe" /> <font-awesome-icon :icon="faGlobe" />
<span>{{ t('auth.languageLabel') }}</span> <span>{{ t('auth.languageLabel') }}</span>
<select v-model="localeValue"> <select v-model="localeValue">
<option v-for="lang in SUPPORTED_LOCALES" :key="lang" :value="lang"> <option v-for="lang in SUPPORTED_LOCALES" :key="lang" :value="lang">
{{ t(`locale.${lang}`) }} {{ t(`locale.${lang}`) }}
</option> </option>
</select> </select>
</label> </label>
<button type="button" class="theme-btn" :aria-label="t('auth.themeToggle')" @click="toggleTheme"> <button type="button" class="theme-btn" :aria-label="t('auth.themeToggle')" @click="toggleTheme">
<font-awesome-icon :icon="isDarkMode ? faMoon : faSun" /> <font-awesome-icon :icon="isDarkMode ? faMoon : faSun" />
<span>{{ themeLabel }}</span> <span>{{ themeLabel }}</span>
</button> </button>
</div> </div>
<h2>{{ t('auth.title') }}</h2> <h2>{{ t('auth.title') }}</h2>
<p class="auth-subtitle">{{ t('auth.subtitle') }}</p> <p class="auth-subtitle">{{ t('auth.subtitle') }}</p>
<form class="auth-form" @submit.prevent="submitForm"> <form class="auth-form" @submit.prevent="submitForm">
<label for="email">{{ t('auth.emailLabel') }}</label> <label for="email">{{ t('auth.emailLabel') }}</label>
<div class="input-wrap"> <div class="input-wrap">
<font-awesome-icon :icon="faEnvelope" class="input-icon" /> <font-awesome-icon :icon="faEnvelope" class="input-icon" />
<input <input id="email" v-model="email" type="email" autocomplete="email"
id="email" :placeholder="t('auth.emailPlaceholder')" required />
v-model="email" </div>
type="email"
autocomplete="email"
:placeholder="t('auth.emailPlaceholder')"
required
/>
</div>
<label for="password">{{ t('auth.passwordLabel') }}</label> <label for="password">{{ t('auth.passwordLabel') }}</label>
<div class="input-wrap"> <div class="input-wrap">
<font-awesome-icon :icon="faLock" class="input-icon" /> <font-awesome-icon :icon="faLock" class="input-icon" />
<input <input id="password" v-model="password" :type="showPassword ? 'text' : 'password'"
id="password" autocomplete="current-password" :placeholder="t('auth.passwordPlaceholder')" required />
v-model="password" <button type="button" class="password-btn"
:type="showPassword ? 'text' : 'password'" :aria-label="showPassword ? t('auth.hidePassword') : t('auth.showPassword')"
autocomplete="current-password" @click="showPassword = !showPassword">
:placeholder="t('auth.passwordPlaceholder')" <font-awesome-icon :icon="showPassword ? faEyeSlash : faEye" />
required </button>
/> </div>
<button
type="button"
class="password-btn"
:aria-label="showPassword ? t('auth.hidePassword') : t('auth.showPassword')"
@click="showPassword = !showPassword"
>
<font-awesome-icon :icon="showPassword ? faEyeSlash : faEye" />
</button>
</div>
<button class="submit-btn" type="submit" :disabled="isLoading"> <button class="submit-btn" type="submit" :disabled="isLoading">
<font-awesome-icon :icon="faRightToBracket" /> <font-awesome-icon :icon="faRightToBracket" />
<span>{{ submitLabel }}</span> <span>{{ submitLabel }}</span>
</button> </button>
</form> </form>
<p class="auth-helper">{{ t('auth.helper') }}</p> <p class="auth-helper">{{ t('auth.helper') }}</p>
<p v-if="errorMessage.length > 0" class="feedback feedback-error">{{ errorMessage }}</p> <p v-if="errorMessage.length > 0" class="feedback feedback-error">{{ errorMessage }}</p>
<p v-if="successMessage.length > 0" class="feedback feedback-success"> <p v-if="successMessage.length > 0" class="feedback feedback-success">
{{ successMessage }} {{ t('auth.tokenSaved') }} {{ successMessage }} {{ t('auth.tokenSaved') }}
</p> </p>
</section> </section>
</div> </div>
</main> </main>
</template> </template>