début du management de l'authentification
This commit is contained in:
parent
d21968e74e
commit
a332d1bbc5
10
package-lock.json
generated
10
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
111
src/App.vue
111
src/App.vue
@ -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>
|
||||||
|
|||||||
@ -3,43 +3,51 @@ 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,
|
||||||
@ -48,6 +56,7 @@ async function submit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
12
src/components/NotFound.vue
Normal file
12
src/components/NotFound.vue
Normal 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>
|
||||||
24
src/composables/useAuth.ts
Normal file
24
src/composables/useAuth.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -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[] = []
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
})
|
});
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user