From be854a79b87009c44dc6ae9da20bf80d92f30e3a Mon Sep 17 00:00:00 2001
From: Colin Kallemein <cakallemein@gmail.com>
Date: Sun, 27 Oct 2024 21:31:50 +0100
Subject: [PATCH] Added reset password modal form

---
 src/components/gui/ResetPassword.vue | 50 ++++++++++++++++++++++++++++
 src/components/utilities/Modal.vue   |  4 +--
 src/screens/Login.vue                |  9 ++++-
 src/services/authentication.ts       |  9 +++++
 src/stores/gameStore.ts              |  7 +++-
 5 files changed, 75 insertions(+), 4 deletions(-)
 create mode 100644 src/components/gui/ResetPassword.vue

diff --git a/src/components/gui/ResetPassword.vue b/src/components/gui/ResetPassword.vue
new file mode 100644
index 0000000..0133a37
--- /dev/null
+++ b/src/components/gui/ResetPassword.vue
@@ -0,0 +1,50 @@
+<template>
+    <Modal :is-modal-open="gameStore.uiSettings.isPasswordResetOpen" @modal:close="() => gameStore.togglePasswordReset()" :modal-width="400" :modal-height="300" :is-resizable="false">
+    <template #modalHeader>
+      <h3 class="m-0 font-medium shrink-0 text-white">Reset Password</h3>
+    </template>
+
+    <template #modalBody>
+      <div class="h-[calc(100%_-_32px)] p-4">
+        <form class="h-full flex flex-col justify-between" @submit.prevent="resetPasswordFunc">
+          <div class="flex flex-col relative">
+            <p>Fill in your email to receive a password reset request.</p>
+            <input type="email" name="email" class="input-field" v-model="email" placeholder="E-mail" />
+            <span v-if="resetPasswordError" class="text-red-200 text-xs absolute top-full mt-1">{{ resetPasswordError }}</span>
+          </div>
+          <div class="grid grid-flow-col justify-stretch gap-4">
+            <button class="btn-empty py-1.5 px-4 min-w-24 inline-block" @click.stop="gameStore.togglePasswordReset">Cancel</button>
+            <button class="btn-cyan py-1.5 px-4 min-w-24 inline-block" type="submit">Send mail</button>
+          </div>
+        </form>
+      </div>
+    </template>
+  </Modal>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from 'vue'
+import { resetPassword } from '@/services/authentication'
+import { useGameStore } from '@/stores/gameStore'
+import Modal from '@/components/utilities/Modal.vue'
+
+const gameStore = useGameStore()
+const email = ref('')
+const resetPasswordError = ref('')
+
+async function resetPasswordFunc() {
+  // check if email is valid
+  if (email.value === '') {
+    resetPasswordError.value = 'Please enter an email'
+    return
+  }
+
+  // send reset password event to server
+  const response = await resetPassword(email.value)
+
+  if (response.success === undefined) {
+    resetPasswordError.value = response.error
+    return
+  }
+}
+</script>
\ No newline at end of file
diff --git a/src/components/utilities/Modal.vue b/src/components/utilities/Modal.vue
index 3ddf0ba..4ba47ca 100644
--- a/src/components/utilities/Modal.vue
+++ b/src/components/utilities/Modal.vue
@@ -2,7 +2,7 @@
   <Teleport to="body">
     <div v-if="isModalOpenRef" class="fixed border-solid border-2 border-gray-500 z-50 flex flex-col backdrop-blur-sm shadow-lg" :style="modalStyle">
       <div @mousedown="startDrag" class="cursor-move p-2.5 flex justify-between items-center border-solid border-0 border-b border-gray-500 relative">
-        <div class="rounded-t-md absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-center bg-cover opacity-90"></div>
+        <div class="rounded-t absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-center bg-cover opacity-90"></div>
         <div class="relative z-10">
           <slot name="modalHeader" />
         </div>
@@ -16,7 +16,7 @@
         </div>
       </div>
       <div class="overflow-hidden grow relative">
-        <div class="rounded-b-md absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center opacity-90"></div>
+        <div class="rounded-b absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center opacity-90"></div>
         <div class="relative z-10 h-full">
           <slot name="modalBody" />
         </div>
diff --git a/src/screens/Login.vue b/src/screens/Login.vue
index 6c13e26..529be5d 100644
--- a/src/screens/Login.vue
+++ b/src/screens/Login.vue
@@ -1,5 +1,6 @@
 <template>
   <div class="relative max-lg:h-dvh flex flex-row-reverse">
+    <ResetPassword />
     <div class="lg:bg-gradient-to-l bg-gradient-to-b from-gray-900 to-transparent w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 z-10"></div>
     <div class="bg-[url('/assets/login/login-bg.png')] w-full lg:w-1/2 h-[35dvh] lg:h-dvh absolute left-0 max-lg:bottom-0 lg:top-0 bg-no-repeat bg-cover bg-center"></div>
     <div class="bg-gray-900 z-20 w-full lg:w-1/2 h-[65dvh] lg:h-dvh relative">
@@ -20,7 +21,7 @@
                 </div>
                 <span v-if="loginError" class="text-red-200 text-xs absolute top-full mt-1">{{ loginError }}</span>
               </div>
-              <button class="inline-flex self-end p-0 text-cyan-300 text-base">Forgot password?</button>
+              <button @click.stop="gameStore.togglePasswordReset" type="button" class="inline-flex self-end p-0 text-cyan-300 text-base">Forgot password?</button>
               <button class="btn-cyan px-0 xs:w-full" type="submit">Play now</button>
 
               <!-- Divider shape -->
@@ -71,6 +72,7 @@ import { onMounted, ref } from 'vue'
 import { login, register } from '@/services/authentication'
 import { useGameStore } from '@/stores/gameStore'
 import { useCookies } from '@vueuse/integrations/useCookies'
+import ResetPassword from '@/components/gui/ResetPassword.vue'
 
 const gameStore = useGameStore()
 const username = ref('')
@@ -115,6 +117,11 @@ async function registerFunc() {
     return
   }
 
+  if (email.value === '') {
+    loginError.value = 'Please enter an email'
+    return
+  }
+
   // send register event to server
   const response = await register(username.value, email.value, password.value)
 
diff --git a/src/services/authentication.ts b/src/services/authentication.ts
index 5d952e2..4c9ab2a 100644
--- a/src/services/authentication.ts
+++ b/src/services/authentication.ts
@@ -25,3 +25,12 @@ export async function login(username: string, password: string) {
     return { error: error.response.data.message }
   }
 }
+
+export async function resetPassword(email: string) {
+  try {
+    const response = await axios.post(`${config.server_endpoint}/reset-password`, { email })
+    return { success: true, token: response.data.token }
+  } catch (error: any) {
+    return { error: error.response.data.message }
+  }
+}
\ No newline at end of file
diff --git a/src/stores/gameStore.ts b/src/stores/gameStore.ts
index ce28efd..5c8c39e 100644
--- a/src/stores/gameStore.ts
+++ b/src/stores/gameStore.ts
@@ -27,7 +27,8 @@ export const useGameStore = defineStore('game', {
       uiSettings: {
         isChatOpen: false,
         isCharacterProfileOpen: false,
-        isGmPanelOpen: false
+        isGmPanelOpen: false,
+        isPasswordResetOpen: false
       }
     }
   },
@@ -71,6 +72,9 @@ export const useGameStore = defineStore('game', {
     toggleCharacterProfile() {
       this.uiSettings.isCharacterProfileOpen = !this.uiSettings.isCharacterProfileOpen
     },
+    togglePasswordReset() {
+      this.uiSettings.isPasswordResetOpen = !this.uiSettings.isPasswordResetOpen
+    },
     initConnection() {
       this.connection = io(config.server_endpoint, {
         secure: !config.development,
@@ -116,6 +120,7 @@ export const useGameStore = defineStore('game', {
       this.gameSettings.isCameraFollowingCharacter = false
       this.uiSettings.isChatOpen = false
       this.uiSettings.isCharacterProfileOpen = false
+      this.uiSettings.isPasswordResetOpen = false
 
       this.world.date = new Date()
       this.world.isRainEnabled = false