initial commit

This commit is contained in:
2026-02-26 21:29:34 +01:00
commit d9d84634e8
72 changed files with 18491 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
<template>
<h1>{{ $t('confirmation.title') }}</h1>
<uiLoading v-if="result === null" />
<div class="confirm-main-text" v-if="result!=null">
<div v-if="result === true">
<p v-if="authStore.isLoggedIn" >{{ $t('confirmation.successConnected') }}
<NuxtLink :to="localePath('/lists')">{{ $t('confirmation.listsLink') }}</NuxtLink>
</p>
<p v-else>{{ $t('confirmation.successNotConnected') }} <NuxtLink :to="localePath('/login')">{{ $t('confirmation.loginLink') }}</NuxtLink></p>
</div>
<div v-else>
<p class="confirm-main-text" v-if="result === 'expired'">{{ $t('confirmation.failureMessage')}}<br/><span>{{ $t('confirmation.failureCauseExpired') }}</span></p>
<p class="confirm-main-text" v-else>{{ $t('confirmation.failureMessage')}}<br/><span>{{ $t('confirmation.failureCauseInvalid') }}</span></p>
<p class="confirm-main-text last">{{ $t('confirmation.failureYouCan')}} <NuxtLink :to="localePath('/signup')">{{ $t('confirmation.failureCreateNewAccount') }}</NuxtLink> {{ $t('confirmation.failureOr') }} <NuxtLink :to="localePath('/')">{{ $t('confirmation.backHome') }}</NuxtLink>
</p>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'confirmation', // utilisation du layout confirm
public: true,
})
import type { ConfirmResult} from '~/types/auth';
const authStore = useAuthStore()
const localePath = useLocalePath()
const loading = ref<boolean>(true)
const result = ref<ConfirmResult | null>(null)
const userConnected = ref<boolean>(false)
onMounted(async () => {
const route = useRoute()
// const user = route.query.user as string
const token = route.query.token as string
if (!token) {
loading.value = false
return
}
try {
result.value = await authStore.confirmUser(token)
if (result.value === true) {
if (authStore.isLoggedIn && authStore.user){
authStore.user.confirmed = true;
}
}
} catch {
throw new Error("Invalid !");
}
finally{
loading.value=false
}
})
</script>
<style scoped lang="scss">
p > span{
color:red;
text-align: center;
display: block;
margin-block: 1.2em;
font-weight: bold;
}
.confirm-main-text:first-of-type{
margin-top: 2em;
}
</style>

49
app/pages/index.vue Normal file
View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
definePageMeta({
public: true,
pageId: 'index'
})
const authStore = useAuthStore();
const localePath = useLocalePath()
</script>
<template>
<h1>{{ $t('index.title') }}</h1>
<h2 class="title">{{ $t('index.subtitle1')}}</h2>
<div class="index-main-text" v-html="$t('index.mainText')"></div>
<div v-if="!authStore.isLoggedIn">
<div class="index-main-text-last">
<i18n-t keypath="index.lastSentenceUnconnect" tag="p">
<template #login>
<NuxtLink :to="localePath('/login')">
{{ $t('index.login') }}
</NuxtLink>
</template>
<template #signup>
<NuxtLink :to="localePath('/signup')">
{{ $t('index.signup') }}
</NuxtLink>
</template>
</i18n-t>
<ButtonGoogle/>
</div>
</div>
<div v-else>
<p>{{ $t('index.lastSentenceConnected') }} <NuxtLink :to="localePath('lists')">{{ $t('index.seeLists') }}.</NuxtLink>
</p>
</div>
</template>
<style>
.button {
background: none;
border: none;
padding: 0;
color: #06c;
text-decoration: underline;
cursor: pointer;
}</style>

51
app/pages/lists.vue Normal file
View File

@@ -0,0 +1,51 @@
<script setup lang="ts">
definePageMeta({
pageId: 'lists'
})
import type {List} from '~/types/lists'
const listStore = useListStore()
// activeTabId réactif
const activeTabId = ref<number | undefined>(undefined)
onMounted(async () => {
if (!listStore.lists.length) {
await listStore.fetchLists()
}
const defaultOpen = listStore.lists.find((l) => l.is_open === true)
activeTabId.value = defaultOpen?.id ?? listStore.lists[0]?.id
})
// Fonction utilitaire pour parser le contenu chiffré
const parseContent = (list: List) => {
try {
return JSON.parse(list.encrypted_content) as any[]
} catch {
return []
}
}
</script>
<template>
<div v-if="!listStore.loading">
<div v-for="list in listStore.lists" :key="list.id">
<h3>{{ list.list_title }}</h3>
<ul>
<li v-for="item in parseContent(list)" :key="item.id ?? item.name ?? item">
{{ item.name ?? JSON.stringify(item) }}
</li>
</ul>
</div>
</div>
<div v-else>
<UiLoading/>
</div>
</template>
<style>
h3{
font-size: 1.2em;
color:blueviolet
}
</style>

114
app/pages/login.vue Normal file
View File

@@ -0,0 +1,114 @@
<script setup lang="ts">
definePageMeta({
public: true, pageId: 'login'
})
const localePath = useLocalePath()
const authStore = useAuthStore() // Store authenticate
const login = ref('')
const password = ref('')
const awaiting = ref(false)
const displayPwdReset=true
const errors = ref({
loginEmpty: false,
passwordEmpty: false,
loginFailed: false,
unconfirmedUser: false,
})
const handleFormSubmit = async() => {
// On retire l'erreur précédente
errors.value.loginFailed = false;
errors.value.unconfirmedUser = false;
awaiting.value = true
// --- Vérification des données du formulaire --- //
// En version raccourcie : on stocke le résultat de la condition dans la variable
errors.value.loginEmpty = ( login.value == "" );
errors.value.passwordEmpty = ( password.value == "" );
// Si une erreur est rencontrée, on n'envoi pas la requete au serveur !
if( !errors.value.loginEmpty && !errors.value.passwordEmpty )
{
// Envoie de la requette à l'endpoint JWT et récupération d'un token de connexion
const success = await authStore.login(login.value, password.value)
if (success) {
// Redirection
await navigateTo(localePath('/lists'))
}
else{
errors.value.loginFailed = true;
awaiting.value = false
}
}
else{
awaiting.value = false
}
}
</script>
<template>
<section>
<h1>{{$t('loginTitle')}}</h1>
<form @submit.prevent="handleFormSubmit">
<InputText
name="login"
:label="$t('loginLabelUser')"
placeholder=""
v-model="login"
>
<template #message>
<p v-if="errors.loginEmpty" class="error">{{$t('loginErrorLoginEmpty')}}</p>
</template>
</InputText>
<InputPassword
name="password"
:label="$t('loginLabelPwd')"
placeholder=""
v-model="password"
displayPwdReset
>
<template #message>
<p v-if="errors.passwordEmpty" class="error">{{ $t('loginErrorPwdEmpty') }}</p>
</template>
</InputPassword>
<div class="connection-error" >
<p v-if="errors.loginFailed">{{ $t('loginErrorLoginFailed') }}</p>
<p v-if="errors.unconfirmedUser">{{ $t('loginErrorUnconfirmedUSer') }}</p>
</div>
<ButtonBase
:disabled="login === '' || password === '' || awaiting"
:loading="awaiting"
>
{{ $t('loginFormBtn') }}
</ButtonBase>
</form>
<p>{{ $t('loginGoogle') }}</p>
<ButtonGoogle/>
</section>
</template>
<style lang="scss" scoped>
form {
display: flex;
flex-direction: column;
padding: 1em;
margin: 0;
}
.connection-error{
height:1.2em;
& > p{
font-weight: bold;
text-align: center;
color: rgb(185, 0, 0);
}
}
</style>

16
app/pages/otherPage.vue Normal file
View File

@@ -0,0 +1,16 @@
<template>
<h1>
value is still : {{ counter }}
</h1>
<NuxtLink to="/">
retour
</NuxtLink>
</template>
<script setup lang="ts">
const { counter } = useCounter()
</script>
<style>
</style>

118
app/pages/passwordReset.vue Normal file
View File

@@ -0,0 +1,118 @@
<template>
<h1>{{$t('pwdReset.title')}}</h1>
<form @submit.prevent="handleSubmit">
<InputPassword
name="password"
placeholder=""
:label="$t('pwdReset.pwdLabel1')"
v-model="password"
@input="check()"
>
<template #message>
<p v-if="pwErrors.isEmpty" class="error">{{ $t('pwdInput.errorPwdEmpty') }}</p>
<p v-if="pwErrors.isntValid" class="error">{{ $t('pwdInput.errorPwdIsntValid') }}</p>
</template>
</InputPassword>
<PasswordChecker/>
<InputPassword
name="passwordVerif"
placeholder=""
:label="$t('pwdReset.pwdLabel2')"
v-model="passwordVerif"
>
<template #message>
<p v-if="pwErrors.doesNotMatch" class="error">{{ $t('pwdInput.pwdsDoesNotMatch') }}</p>
<p v-if="pwErrors.verifyEmpty" class="error">{{ $t('pwdInput.pwdConfirmEmpty') }}</p>
</template>
</InputPassword>
<ButtonBase
:loading="loading"
:disabled="loading || success === true">
{{ $t('pwdReset.formBtn') }}
</ButtonBase>
</form>
<div v-if="success">
<p>{{ $t('pwdReset.successMessage') }}</p>
<NuxtLink :to="localePath('login')">{{ $t('pwdReset.loginLink') }}</NuxtLink>
</div>
<div v-else-if="success===false" class="error">
<p>{{ $t('pwdReset.errorMessage') }}</p>
</div>
</template>
<script setup lang="ts">
definePageMeta({
public: true,
})
const route = useRoute()
const localePath=useLocalePath()
const token = ref(route.query.token)
const authStore = useAuthStore()
//** pwdReset up management **/
const passwordToolBox = usePasswordToolBoxStore()
// password relative functions
const check = () => {
passwordToolBox.updatePassword(password.value)
}
const password=ref('')
const passwordVerif=ref('')
const pwErrors = ref({
isEmpty: false as boolean,
verifyEmpty: false as boolean,
isntValid: false as boolean,
doesNotMatch:false as boolean,
})
const loading = ref(false as boolean)
const success = ref(null as null|boolean);
// form relative functions
const handleSubmit = async() => {
loading.value = true
//réinitialisation des erreurs
pwErrors.value.isEmpty = false;
pwErrors.value.verifyEmpty = false;
pwErrors.value.isntValid = false;
pwErrors.value.doesNotMatch = false;
// Check errors
pwErrors.value.isEmpty = ( password.value == "" );
pwErrors.value.verifyEmpty = ( passwordVerif.value == "" );
pwErrors.value.isntValid = !passwordToolBox.isPasswordValid();
pwErrors.value.doesNotMatch = ( password.value != passwordVerif.value );
if ( !pwErrors.value.isEmpty &&
!pwErrors.value.verifyEmpty &&
!pwErrors.value.isntValid &&
!pwErrors.value.doesNotMatch) {
success.value = await authStore.pwdReset(password.value, token.value)
}
loading.value = false
password.value = ""
passwordVerif.value = ""
}
</script>
<style scoped lang="scss">
form {
display: flex;
flex-direction: column;
padding: 1em;
margin: 0;
}
.error{
& > p{
font-weight: bold;
color: rgb(185, 0, 0);
}
}
</style>

View File

@@ -0,0 +1,105 @@
<template>
<h1>{{$t('pwdResetRequest.title')}}</h1>
<form @submit.prevent="handleFormSubmit()">
<InputEmail
v-model="email"
name="email"
:label="$t('signupLabelEmail')"
placeholder=""
:class="emailStatus"
>
<template #message>
<p v-if="emailErrors.isEmpty" class="error">{{ $t('signupErrorEmailEmpty') }}</p>
<p v-else-if="emailErrors.isntValid" class="error">{{ $t('signupErrorEmailIsntValid') }}</p>
<p v-else-if="emailValid" class="success">{{ $t('signupEmailIsValid') }}</p>
</template>
</InputEmail>
<ButtonBase
:disabled="!emailValid || awaiting"
:loading="awaiting">
{{ $t('pwdResetRequest.formBtn') }}
</ButtonBase>
<div class="error" v-if="formErrors.submitFailed">
<p>{{ $t('pwdResetRequest.pb') }}</p>
<p>{{ $t('pwdResetRequest.message') }} {{formErrors.message}}</p>
</div>
<div class="success" v-if="success">
<p>{{ $t('pwdResetRequest.successMessage') }}</p>
<p>{{ $t('pwdResetRequest.linkValidity') }}</p>
<p>{{ $t('pwdResetRequest.seeU') }}</p>
</div>
</form>
</template>
<script setup lang="ts">
definePageMeta({
public: true
})
const { locale } = useI18n()
const authStore = useAuthStore()
const email = ref("")
const awaiting = ref(false)
const emailErrors = ref({
isEmpty: false as boolean,
isntValid: false as boolean,
})
const formErrors = ref({
submitFailed: false as boolean,
message: null as string | null,
})
const success = ref(false)
const emailValid = computed(() => isValidEmail(email.value))
const emailStatus = computed(() => {
return emailValid.value ? "mail-is-valid" : "mail-is-invalid"
})
function isValidEmail(value: string):boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
}
const handleFormSubmit = async() => {
//* Errors reinitialisation...
awaiting.value = true
emailErrors.value.isEmpty = false;
emailErrors.value.isntValid = false;
formErrors.value.submitFailed = false;
formErrors.value.message = '';
success.value = false;
//* Now come the verifications
emailErrors.value.isEmpty = (email.value == '')
emailErrors.value.isntValid = ( !isValidEmail(email.value) );
if (emailErrors.value.isEmpty || emailErrors.value.isntValid){
return false;
}
const response = await authStore.pwdResetResquest(email.value, locale.value);
//* If we have a message, it means we have an error...
if (response == true){
success.value = true;
email.value = "";
awaiting.value = false;
}
else {
formErrors.value.submitFailed = true;
formErrors.value.message = authStore.error;
awaiting.value = false;
success.value = false;
}
}
</script>
<style scoped lang="scss">
form {
display: flex;
flex-direction: column;
padding: 1em;
margin: 0;
}
</style>

109
app/pages/profile.vue Normal file
View File

@@ -0,0 +1,109 @@
<template>
<div class="profile-page">
<h1>{{ $t('profile.title') }}</h1>
<!-- Onglets -->
<div role="tablist" aria-label="Profil utilisateur" class="tabs">
<button
:class="['tab', activeTab === 'general' ? 'active' : '']"
role="tab"
:aria-selected="activeTab === 'general'"
aria-controls="tab-general"
id="tab-button-general"
@click="activeTab = 'general'; authStore.user.sudo_token = null"
>
{{ $t('profile.tabGeneral') }}
</button>
<button
:class="['tab', activeTab === 'redzone' ? 'active' : '']"
class='redzone'
role="tab"
:aria-selected="activeTab === 'redzone'"
aria-controls="tab-redzone"
id="tab-button-redzone"
@click="activeTab = 'redzone'; authStore.user.sudo_token = null"
>
{{ $t('profile.tabRedZone')}}
</button>
</div>
<!-- Contenu des onglets -->
<div class="tab-panels">
<!-- Onglet Général -->
<section
v-show="activeTab === 'general'"
role="tabpanel"
aria-labelledby="tab-button-general"
id="tab-general"
>
<ProfileGeneral />
</section>
<!-- Onglet RED ZONE -->
<section
v-show="activeTab === 'redzone'"
role="tabpanel"
aria-labelledby="tab-button-redzone"
id="tab-redzone"
>
<ProfileRedZone />
</section>
</div>
</div>
</template>
<script setup lang="ts">
const authStore = useAuthStore()
// Onglet actif (TypeScript infère automatiquement le type string)
const activeTab = ref('general')
</script>
<style scoped lang="scss">
/* Onglets */
.tabs {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
}
.tab {
padding: 0.5rem 1rem;
background: none;
border: none;
border-bottom: 3px solid transparent;
cursor: pointer;
font-weight: bold;
font-size: 1rem;
}
//
.tab:hover {
background-color: #f5f5f5;
}
.tab.active {
border-bottom-color: blueviolet;
color: blueviolet;
}
button.redzone{
color:red;
&.active{
border-bottom-color: red;
color:red;
}
}
/* Contenu onglet */
.tab-panels section {
animation: fadeIn 0.3s ease;
}
/* Animation simple */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
</style>

160
app/pages/signup.vue Executable file
View File

@@ -0,0 +1,160 @@
<template>
<section>
<UiModale @close="closeModal" :modalActive="modalActive">
<div class="modal-content">
<h1 class="modal-title">{{ $t('modalTitle') }}</h1>
<p>{{ $t('modalText') }}</p>
</div>
</UiModale>
<h1>{{ $t('signupTitle') }}</h1>
<form @submit.prevent="handleSubmit">
<InputEmail
v-model="email"
name="email"
:label="$t('signupLabelEmail')"
placeholder=""
:class="emailStatus"
@blur="touched = true"
>
<template #message>
<p v-if="emailErrors.isEmpty" class="error">{{ $t('signupErrorEmailEmpty') }}</p>
<p v-else-if="emailErrors.isntValid" class="error">{{ $t('signupErrorEmailIsntValid') }}</p>
<p v-else-if="emailValid" class="success">{{ $t('signupEmailIsValid') }}</p>
</template>
</InputEmail>
<InputPassword
name="password"
placeholder=""
:label="$t('signupLabelPwd')"
v-model="password"
@input="check()"
>
<template #message>
<p v-if="pwErrors.isEmpty" class="error">{{ $t('signupErrorPwdEmpty') }}</p>
<p v-if="pwErrors.isntValid" class="error">{{ $t('signupErrorPwsIsntValid') }}</p>
</template>
</InputPassword>
<PasswordChecker/>
<InputPassword
name="passwordVerif"
placeholder=""
:label="$t('signupLabelConfirmPwd')"
v-model="passwordVerif"
>
<template #message>
<p v-if="pwErrors.doesNotMatch" class="error">{{ $t('signupErrorPwdsDoesNotMatch') }}</p>
<p v-if="pwErrors.verifyEmpty" class="error">{{ $t('signupErrorPwdConfirmEmpty') }}</p>
</template>
</InputPassword>
<ButtonBase>
{{ $t('signupFormBtn') }}
</ButtonBase>
</form>
</section>
</template>
<script setup lang="ts">
definePageMeta({
public: true
})
const { locale } = useI18n()
//** Modale management **//
const localePath=useLocalePath()
const modalActive = ref(false)
const closeModal = () => {
modalActive.value = false
navigateTo(localePath('lists'))
}
//** Sign up management **/
const authStore = useAuthStore()
const passwordToolBox = usePasswordToolBoxStore()
// email relative functions
const email = ref("")
const touched = ref(false)
function isValidEmail(value: string) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
}
const emailValid = computed(() => touched.value && isValidEmail(email.value))
const emailErrors = ref({
isEmpty: false as boolean,
isntValid: false as boolean
})
//const emailInvalid = computed(() => touched.value && !isValidEmail(email.value))
const emailStatus = computed(() => {
if (!touched.value) return ""
return emailValid.value ? "mail-is-valid" : "mail-is-invalid"
})
// password relative functions
const check = () => {
passwordToolBox.updatePassword(password.value)
}
const password=ref('')
const passwordVerif=ref('')
const pwErrors = ref({
isEmpty: false as boolean,
verifyEmpty: false as boolean,
isntValid: false as boolean,
doesNotMatch:false as boolean,
})
// form relative functions
const handleSubmit = async() => {
//réinitialisation des erreurs
emailErrors.value.isEmpty = false
emailErrors.value.isntValid = false
pwErrors.value.isEmpty = false;
pwErrors.value.verifyEmpty = false;
pwErrors.value.isntValid = false;
pwErrors.value.doesNotMatch = false;
// Check errors
emailErrors.value.isEmpty = ( email.value == "" );
emailErrors.value.isntValid = !isValidEmail(email.value)
pwErrors.value.isEmpty = ( password.value == "" );
pwErrors.value.verifyEmpty = ( passwordVerif.value == "" );
pwErrors.value.isntValid = !passwordToolBox.isPasswordValid();
pwErrors.value.doesNotMatch = ( password.value != passwordVerif.value );
if ( !emailErrors.value.isEmpty &&
!emailErrors.value.isntValid &&
!pwErrors.value.isEmpty &&
!pwErrors.value.verifyEmpty &&
!pwErrors.value.isntValid &&
!pwErrors.value.doesNotMatch) {
const success = await authStore.register(email.value, password.value, locale.value)
if (success) {
modalActive.value=true;
}
}
}
</script>
<style scoped lang="scss">
form {
display: flex;
flex-direction: column;
padding: 1em;
margin: 0;
}
.connection-error{
height:1.2em;
& > p{
font-weight: bold;
text-align: center;
color: rgb(185, 0, 0);
}
}
</style>