Skip to content

Commit

Permalink
Session logic, login, update dependences
Browse files Browse the repository at this point in the history
  • Loading branch information
AstroCorp committed Sep 16, 2024
1 parent fcdfcf8 commit 40d5884
Show file tree
Hide file tree
Showing 12 changed files with 680 additions and 302 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
NODE_ENV=development

NUXT_SESSION_NAME=session
NUXT_SESSION_PASSWORD=
NUXT_SESSION_TIME=604800 # 7 días en segundos

BACKEND_URL=http://localhost:8080
FRONTEND_URL=http://localhost:3000
Expand Down
15 changes: 11 additions & 4 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,21 @@ export default defineNuxtConfig({
runtimeConfig: {
// Private keys are only available on the server
nodeEnv: process.env.NODE_ENV,
nuxtSessionName: process.env.NUXT_SESSION_NAME,
nuxtSessionPassword: process.env.NUXT_SESSION_PASSWORD,
nuxtSessionTime: process.env.NUXT_SESSION_TIME,

// Public keys that are exposed to the client
public: {
backendUrl: process.env.BACKEND_URL,
frontendUrl: process.env.FRONTEND_URL,
mailUsername: process.env.MAIL_USERNAME,
},

session: {
name: process.env.NUXT_SESSION_NAME,
password: process.env.NUXT_SESSION_PASSWORD,
},
},
vite: {
server: {
Expand All @@ -47,12 +54,12 @@ export default defineNuxtConfig({
},
],
modules: [
"@nuxtjs/i18n",
"@nuxtjs/i18n",
"@nuxt/image",
"@nuxtjs/mdc",
"nuxt-auth-utils",
"@nuxtjs/mdc",
"nuxt-auth-utils",
"nuxt-icons",
],
],
i18n: {
vueI18n: './src/i18n/i18n.config.ts',
locales: [
Expand Down
18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,21 @@
},
"dependencies": {
"@nuxt/image": "^1.8.0",
"@nuxtjs/i18n": "^8.5.1",
"@nuxtjs/mdc": "^0.8.3",
"@nuxtjs/i18n": "^8.5.3",
"@nuxtjs/mdc": "^0.9.0",
"@vueuse/components": "^11.0.3",
"@vueuse/core": "^11.0.3",
"nuxt": "^3.13.0",
"nuxt-auth-utils": "^0.3.5",
"nuxt": "^3.13.1",
"nuxt-auth-utils": "^0.3.8",
"nuxt-icons": "^3.2.1",
"vue": "^3.4.38",
"vue-router": "^4.4.3",
"vue": "^3.5.5",
"vue-router": "^4.4.5",
"zod": "^3.23.8"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.8",
"@tailwindcss/forms": "^0.5.9",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.41",
"tailwindcss": "^3.4.10"
"postcss": "^8.4.47",
"tailwindcss": "^3.4.11"
}
}
26 changes: 26 additions & 0 deletions src/composables/useJwt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createSharedComposable } from "@vueuse/core";
import type { Payload, SignPayload } from "~/types/useJwt";

const useJwt = createSharedComposable(() => {
const extractJwtPayload = (token: string) => {
const [ header, payload, signature ] = token.split('.');
const payloadData = JSON.parse(atob(payload));

return payloadData;
}

const extractTokenData = (token: string): Payload => {
return extractJwtPayload(token);
}

const extractSignData = (token: string): SignPayload => {
return extractJwtPayload(token);
}

return {
extractTokenData,
extractSignData,
};
});

export default useJwt;
37 changes: 32 additions & 5 deletions src/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,36 @@
export default defineNuxtRouteMiddleware(() => {
const { loggedIn } = useUserSession();
export default defineNuxtRouteMiddleware(async (middleware) => {
const nuxtApp = useNuxtApp();
const localeRoute = useLocaleRoute();
const { loggedIn, fetch, session, clear } = useUserSession();
const { extractTokenData } = useJwt();

if (!loggedIn.value) {
return navigateTo("/login");
// Si el usuario está logueado, comprueba si los tokens están expirados o necesitan ser renovados
if (loggedIn.value) {
const accessTokenData = extractTokenData(session.value.access_token);
const refreshTokenData = extractTokenData(session.value.refresh_token);

// Date.now() es en milisegundos y exp en segundos,
// por eso se multiplica por 1000
const accessTokenIsExpired = Date.now() >= accessTokenData.exp * 1000;
const refreshTokenIsExpired = Date.now() >= refreshTokenData.exp * 1000;

const tokensAreExpired = accessTokenIsExpired && refreshTokenIsExpired;
const tokensNeedRefresh = accessTokenIsExpired && !refreshTokenIsExpired;

if (tokensAreExpired) {
await clear();
}

if (tokensNeedRefresh) {
await fetch();
}
}

return null;
// Si el usuario no está logueado, lo redirige al login
if (!loggedIn.value) {
const loginRoute = localeRoute('login', nuxtApp.$i18n.locale);
const loginPath = loginRoute != null ? loginRoute.path : '/';

return navigateTo(loginPath);
}
});
7 changes: 5 additions & 2 deletions src/pages/(user)/library.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
const { t } = useI18n();
const { session, user, clear } = useUserSession();
defineI18nRoute({
paths: {
Expand All @@ -24,7 +25,9 @@ useHead({
</script>

<template>
<div>
Content
<div class="p-2">
<div class="mb-4">{{ session }}</div>
<div class="mb-4">{{ user }}</div>
<button class="border p-2 cursor-pointer hover:bg-slate-100" @click="clear">Logout</button>
</div>
</template>
21 changes: 10 additions & 11 deletions src/pages/auth/login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
const { t, locale } = useI18n();
const localeRoute = useLocaleRoute();
const config = useRuntimeConfig();
const { fetch } = useUserSession();
defineI18nRoute({
paths: {
Expand All @@ -20,18 +21,15 @@ useHead({
],
});
const libraryRoute = computed(() => {
const route = localeRoute('library', locale.value);
return route != null ? route.path : '/';
});
const loginForm = ref({
email: '',
password: '',
});
const showLoginError = ref(false);
const submitForm = async () => {
const submitForm = async (event: Event) => {
event.preventDefault();
showLoginError.value = false;
try {
Expand All @@ -44,11 +42,12 @@ const submitForm = async () => {
return;
}
// Refrescamos la sesión del usuario
const { fetch } = useUserSession();
await fetch();
await fetch();
const libraryRoute = localeRoute('library', locale.value);
const libraryPath = libraryRoute != null ? libraryRoute.path : '/';
await navigateTo(libraryRoute.value);
await navigateTo(libraryPath);
};
</script>

Expand All @@ -60,7 +59,7 @@ const submitForm = async () => {
</div>

<div class="flex flex-row items-center lg:px-8 w-full min-h-svh sm:w-1/2 md:w-2/5 xl:w-2/6 bg-white">
<form class="w-full px-6 py-4" @submit.prevent="submitForm">
<form class="w-full px-6 py-4" @submit="submitForm">
<NuxtLinkLocale to="/">
<nuxt-icon name="logo" class="flex w-1/3 mx-auto mb-4 xl:mb-5" />
</NuxtLinkLocale>
Expand Down
10 changes: 8 additions & 2 deletions src/server/api/auth/login.post.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { LoginResponse } from "~/types/auth";
import { LoginAndRefreshResponse } from "~/types/auth";

export default defineEventHandler(async (event) => {
const config = useRuntimeConfig();
const body = await readBody(event);

const response = await $fetch<LoginResponse>(config.public.backendUrl + '/auth/login', {
const response = await $fetch<LoginAndRefreshResponse>(config.public.backendUrl + '/auth/login', {
method: 'POST',
body: JSON.stringify({
email: body.email,
Expand All @@ -21,5 +21,11 @@ export default defineEventHandler(async (event) => {
},
access_token: response.access_token,
refresh_token: response.refresh_token,
}, {
maxAge: Number(config.nuxtSessionTime),
});

return {
success: true,
};
});
54 changes: 54 additions & 0 deletions src/server/plugins/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import useJwt from "~/composables/useJwt";
import { LoginAndRefreshResponse } from "~/types/auth";

export default defineNitroPlugin(() => {
const config = useRuntimeConfig();
const { extractTokenData } = useJwt();

// Se ejecuta al comprobar la sesión al usar SSR desde el composable (/api/_auth/session)
// o al usar useUserSession().fetch()
sessionHooks.hook('fetch', async (session, event) => {
const accessTokenData = extractTokenData(session.access_token);
const refreshTokenData = extractTokenData(session.refresh_token);

// Date.now() es en milisegundos y exp en segundos,
// por eso se multiplica por 1000
const accessTokenIsExpired = Date.now() >= accessTokenData.exp * 1000;
const refreshTokenIsExpired = Date.now() >= refreshTokenData.exp * 1000;

const tokensAreExpired = accessTokenIsExpired && refreshTokenIsExpired;
const tokensNeedRefresh = accessTokenIsExpired && !refreshTokenIsExpired;

// Si los tokens están expirados cerramos la sesión
if (tokensAreExpired) {
await clearUserSession(event);
}

// Si solo el token de acceso está expirado y el de refresco no, refrescamos los tokens
if (tokensNeedRefresh) {
try {
const response = await $fetch<LoginAndRefreshResponse>(config.public.backendUrl + '/auth/refresh', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + session.refresh_token,
},
});

await replaceUserSession(event, {
user: {
email: response.user.email,
avatar: response.user.avatar,
isAdmin: response.user.isAdmin,
isVerified: response.user.isVerified,
},
access_token: response.access_token,
refresh_token: response.refresh_token,
}, {
maxAge: Number(config.nuxtSessionTime),
});
} catch (error) {
await clearUserSession(event);
}
}
});
});
2 changes: 1 addition & 1 deletion src/types/auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ declare module '#auth-utils' {
interface UserSession extends LoginData {}
}

export interface LoginResponse extends LoginData {}
export interface LoginAndRefreshResponse extends LoginData {}
12 changes: 12 additions & 0 deletions src/types/useJwt.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export interface Payload {
user_id: number;
iat: number;
exp: number;
}

export interface SignPayload {
user_id: number;
type: string;
iat: number;
exp: number;
}
Loading

0 comments on commit 40d5884

Please sign in to comment.