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", "version": "0.0.0",
"dependencies": { "dependencies": {
"axios": "^1.13.2", "axios": "^1.13.2",
"jwt-decode": "^4.0.0",
"vue": "^3.5.17", "vue": "^3.5.17",
"vue-router": "^4.6.3", "vue-router": "^4.6.3",
"vue-toast-notification": "^3.1.3" "vue-toast-notification": "^3.1.3"
@ -3822,6 +3823,15 @@
"graceful-fs": "^4.1.6" "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": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",

View File

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

View File

@ -1,14 +1,119 @@
<script setup lang="ts"> <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> </script>
<template> <template>
<link rel="preconnect" href="https://fonts.googleapis.com"> <nav>
<link rel="preconnect" href="https://fonts.gstatic.com"> <RouterLink class="nav-logo-div nav-link" to="/">My book list</RouterLink>
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet"> <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/> <RouterView/>
</template> </template>
<style> <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> </style>

View File

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

View File

@ -15,12 +15,11 @@ onMounted(async () => {
<template> <template>
<div v-for="book in books" class="card"> <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"> <div class="content">
<a href="#"> <a href="#">
<span class="title">{{ book.title }}</span> <span class="title">{{ book.title }}</span>
</a> </a>
<p class="desc"> <p class="desc">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Recusandae dolores, possimus Lorem ipsum dolor sit amet, consectetur adipisicing elit. Recusandae dolores, possimus
pariatur animi temporibus nesciunt praesentium 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 { export class User {
id: number = 0; id: number = 0;
username: string = "" username: string = ""
first_name: string = ""
last_name: string = ""
email: string = "" email: string = ""
profile_picture: string = ""
books: Book[] = []
} }

View File

@ -1,32 +1,42 @@
import { createWebHistory, createRouter } from 'vue-router' import { createWebHistory, createRouter } from "vue-router";
import Authenticate from './components/Authenticate.vue' import Authenticate from "./components/Authenticate.vue";
import Home from './components/Home.vue' import Home from "./components/Home.vue";
import NotFound from "./components/NotFound.vue";
const routes = [ const routes = [
{ {
path: '/', path: "/",
component: Home, component: Home,
name: "Home", name: "Home",
}, },
{ {
path: '/authenticate', path: "/login",
component: Authenticate, component: Authenticate,
name: "Authenticate", name: "Authenticate",
meta: { guestOnly: true } meta: { guestOnly: true },
}, },
] {
path: "/:pathMatch(.*)*",
component: NotFound,
name: "Not found",
},
];
export const router = createRouter({ export const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes, routes,
}) });
router.beforeEach((to, from) => { router.beforeEach((to, from) => {
const authenticated = sessionStorage.getItem('access'); const authenticated = sessionStorage.getItem("access");
if (to.meta.guestOnly && authenticated) { if (to.path === "/login" && authenticated) {
return { name: 'Home' }; return { name: "Home " };
}
if (to.path === "/logout" && !authenticated) {
return { name: "Home" };
} }
return true; 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) { async authenticate(username: string, password: string) {
return await axios return await axios
.post('token/', { .post('token/', {
@ -20,8 +32,7 @@ export class AuthenticationService {
password: password, password: password,
}) })
.then((res) => { .then((res) => {
localStorage.setItem('refresh', res.data.refresh) return res.data;
sessionStorage.setItem('access', res.data.access)
}) })
.catch((error) => { .catch((error) => {
throw error; throw error;