début du management de l'authentification

This commit is contained in:
Guamss 2025-12-07 15:13:46 +01:00
parent d21968e74e
commit a332d1bbc5
10 changed files with 236 additions and 49 deletions

10
package-lock.json generated
View File

@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"axios": "^1.13.2",
"jwt-decode": "^4.0.0",
"vue": "^3.5.17",
"vue-router": "^4.6.3",
"vue-toast-notification": "^3.1.3"
@ -3822,6 +3823,15 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/jwt-decode": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",

View File

@ -13,6 +13,7 @@
},
"dependencies": {
"axios": "^1.13.2",
"jwt-decode": "^4.0.0",
"vue": "^3.5.17",
"vue-router": "^4.6.3",
"vue-toast-notification": "^3.1.3"

View File

@ -1,14 +1,119 @@
<script setup lang="ts">
import { useToast } from 'vue-toast-notification';
import { useAuth } from '@/composables/useAuth';
import { User } from './models/user';
import { onMounted, ref, watch } from 'vue';
import { AuthenticationService } from './services/authentication.service';
const authenticatedUser = ref<User | undefined>(undefined)
const API_URL = import.meta.env.VITE_MBL_API_URL
const { isAuthenticated, logout } = useAuth();
const toastService = useToast();
const authenticationService = new AuthenticationService();
const fetchUser = async () => {
if (isAuthenticated.value) {
try {
authenticatedUser.value = await authenticationService.findMe();
} catch (error) {
console.error(error);
}
} else {
authenticatedUser.value = undefined; // On vide si déconnecté
}
};
onMounted(() => {
fetchUser();
});
watch(isAuthenticated, () => {
fetchUser();
});
function handleLogout() {
logout();
toastService.info('Vous êtes maintenant déconnecté', {
position: 'bottom-right',
pauseOnHover: true,
dismissible: true,
});
}
</script>
<template>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
<nav>
<RouterLink class="nav-logo-div nav-link" to="/">My book list</RouterLink>
<div class="nav-links-div">
<RouterLink class="nav-link" to="/">Accueil</RouterLink>
<RouterLink class="nav-link" to="/">About</RouterLink>
<RouterLink class="nav-link" to="/">Blabla</RouterLink>
</div>
<div class="nav-signin-div">
<RouterLink v-if="!isAuthenticated" class="nav-link" to="/login">Se connecter</RouterLink>
<!-- <a v-else @click="handleLogout" class="nav-link" style="cursor:pointer">Se déconnecter</a> -->
<div class="user-section" v-else>
<img class="profile-picture" :src="API_URL + authenticatedUser?.profile_picture"></img>
<span>{{ authenticatedUser?.username }}</span>
</div>
</div>
</nav>
<RouterView/>
</template>
<style>
@import "styles.css";
@import "styles.css";
.user-section {
border-radius: 6px;
padding-bottom: 4px;
padding-top: 4px;
padding-right: 8px;
padding-left: 8px;
cursor: pointer;
display: flex;
flex-direction: row;
gap: 10px;
align-items: center;
}
.user-section:hover {
background-color: light-dark(white, #5e5e5e);
}
.profile-picture {
width: 2em;
height: 2em;
border-radius: 1em;
}
.nav-logo-div {
font-size: 20px;
font-weight: bold;
}
.nav-link {
text-decoration: none;
color: light-dark(#121212, #efefec);
}
.nav-link:visited {
color: light-dark(#121212, #efefec);
}
nav {
align-items: center;
border-radius: 10px;
background-color: light-dark(#efedea, #282828);
margin-bottom: 3em;
padding: 1em;
display: flex;
justify-content: space-around;
}
.nav-links-div {
display: flex;
gap: 10em;
}
</style>

View File

@ -3,48 +3,57 @@ import { AuthenticationService } from '@/services/authentication.service';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useToast } from 'vue-toast-notification';
import { useAuth } from '@/composables/useAuth';
const username = ref<string>('')
const password = ref<string>('')
const usernameErrorDetail = ref<string>('')
const passwordErrorDetail = ref<string>('')
const generalErrorDetail = ref<string>('')
const username = ref<string>('');
const password = ref<string>('');
const usernameErrorDetail = ref<string>('');
const passwordErrorDetail = ref<string>('');
const generalErrorDetail = ref<string>('');
const toastService = useToast();
const router = useRouter()
const authenticationService = new AuthenticationService()
const router = useRouter();
const authenticationService = new AuthenticationService();
const { login } = useAuth();
async function submit() {
try {
await authenticationService.authenticate(username.value, password.value)
const responseData = await authenticationService.authenticate(username.value, password.value);
login(responseData.access, responseData.refresh);
toastService.success(`Heureux de vous revoir ${username.value}!`, {
position: 'top-right',
position: 'bottom-right',
pauseOnHover: true,
dismissible: true,
})
});
router.push('/');
} catch (error: any) {
usernameErrorDetail.value = ''
passwordErrorDetail.value = ''
generalErrorDetail.value = ''
const errorCode = error.status
const errorDetails = error.response.data
if (errorCode === 400) {
if (errorDetails.username) {
usernameErrorDetail.value = errorDetails.username[0] // le champ username est vide
}
if (errorDetails.password) {
passwordErrorDetail.value = errorDetails.password[0] // le champ password est vide
}
} else {
generalErrorDetail.value = errorDetails.detail
toastService.error(`${generalErrorDetail.value}`, {
position: 'top-right',
pauseOnHover: true,
dismissible: true,
})
if (error.response) {
const errorCode = error.status || error.response.status;
const errorDetails = error.response.data;
if (errorCode === 400) {
if (errorDetails.username) {
usernameErrorDetail.value = errorDetails.username[0]
}
if (errorDetails.password) {
passwordErrorDetail.value = errorDetails.password[0]
}
} else {
generalErrorDetail.value = errorDetails.detail || 'Erreur inconnue'
toastService.error(`${generalErrorDetail.value}`, {
position: 'top-right',
pauseOnHover: true,
dismissible: true,
})
}
}
}
}
@ -55,7 +64,7 @@ async function submit() {
<form>
<h1>Connexion</h1>
<label>Nom d'utilisateur</label>
<input @keyup.enter="submit" v-model="username" placeholder="Alexis" type="text" required>
<input @keyup.enter="submit" v-model="username" placeholder="Yves" type="text" required>
<ul>
<li v-if="usernameErrorDetail" style="color:red">{{ usernameErrorDetail }}</li>
</ul>

View File

@ -15,12 +15,11 @@ onMounted(async () => {
<template>
<div v-for="book in books" class="card">
<img :src="API_URL + '/' + book.illustration" class="image"></img>
<img :src="API_URL + book.illustration" class="image"></img>
<div class="content">
<a href="#">
<span class="title">{{ book.title }}</span>
</a>
<p class="desc">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Recusandae dolores, possimus
pariatur animi temporibus nesciunt praesentium

View File

@ -0,0 +1,12 @@
<script setup lang="ts">
</script>
<template>
<h1>La ressource que vous cherchez n'existe pas !</h1>
</template>
<style scoped>
h1 {
text-align: center;
}
</style>

View File

@ -0,0 +1,24 @@
import { ref } from 'vue';
const isAuthenticated = ref<boolean>(!!sessionStorage.getItem('access'));
export function useAuth() {
function login(accessToken: string, refreshToken: string) {
sessionStorage.setItem('access', accessToken);
localStorage.setItem('refresh', refreshToken); // Il dure 1j, UN JOUR !!!
isAuthenticated.value = true;
}
function logout() {
sessionStorage.removeItem('access');
localStorage.removeItem('refresh');
isAuthenticated.value = false;
}
return {
isAuthenticated,
login,
logout
};
}

View File

@ -1,5 +1,11 @@
import type { Book } from "./book";
export class User {
id: number = 0;
username: string = ""
first_name: string = ""
last_name: string = ""
email: string = ""
profile_picture: string = ""
books: Book[] = []
}

View File

@ -1,32 +1,42 @@
import { createWebHistory, createRouter } from 'vue-router'
import Authenticate from './components/Authenticate.vue'
import Home from './components/Home.vue'
import { createWebHistory, createRouter } from "vue-router";
import Authenticate from "./components/Authenticate.vue";
import Home from "./components/Home.vue";
import NotFound from "./components/NotFound.vue";
const routes = [
{
path: '/',
path: "/",
component: Home,
name: "Home",
},
{
path: '/authenticate',
path: "/login",
component: Authenticate,
name: "Authenticate",
meta: { guestOnly: true }
meta: { guestOnly: true },
},
]
{
path: "/:pathMatch(.*)*",
component: NotFound,
name: "Not found",
},
];
export const router = createRouter({
history: createWebHistory(),
routes,
})
});
router.beforeEach((to, from) => {
const authenticated = sessionStorage.getItem('access');
const authenticated = sessionStorage.getItem("access");
if (to.meta.guestOnly && authenticated) {
return { name: 'Home' };
if (to.path === "/login" && authenticated) {
return { name: "Home " };
}
if (to.path === "/logout" && !authenticated) {
return { name: "Home" };
}
return true;
})
});

View File

@ -13,6 +13,18 @@ export class AuthenticationService {
})
}
async findMe() {
const headers = { 'Authorization': `Bearer ${sessionStorage.getItem('access')}` };
return await axios
.get('profile', { headers })
.then((res) => {
return res.data as User;
})
.catch((error) => {
throw error;
})
}
async authenticate(username: string, password: string) {
return await axios
.post('token/', {
@ -20,8 +32,7 @@ export class AuthenticationService {
password: password,
})
.then((res) => {
localStorage.setItem('refresh', res.data.refresh)
sessionStorage.setItem('access', res.data.access)
return res.data;
})
.catch((error) => {
throw error;