Browse Source

Feature: Mobile - Automatic logout after a not authenticated response.

Dominik Klein 3 years ago
parent
commit
94a1d82858

+ 17 - 6
app/frontend/apps/mobile/App.vue

@@ -20,11 +20,7 @@ import useAuthenticatedStore from '@common/stores/authenticated'
 import useSessionIdStore from '@common/stores/session/id'
 import useMetaTitle from '@common/composables/useMetaTitle'
 import { useRoute, useRouter } from 'vue-router'
-
-// TODO ... maybe show some special message, if the session was removed from a other place.
-// unauthorized () {
-//     return !this.$store.getters.accessToken && this.$store.getters.authenticated;
-// },
+import { computed, watch } from 'vue'
 
 const router = useRouter()
 const route = useRoute()
@@ -37,7 +33,22 @@ useMetaTitle().initializeMetaTitle()
 const applicationLoaded = useApplicationLoadedStore()
 applicationLoaded.setLoaded()
 
-// Add a watcher for authenticated change.
+const invalidatedSession = computed(() => {
+  return !sessionId.value && authenticated.value
+})
+
+watch(invalidatedSession, () => {
+  authenticated.clearAuthentication()
+
+  router.replace({
+    name: 'Login',
+    params: {
+      invalidatedSession: '1',
+    },
+  })
+})
+
+// Add a watcher for authenticated changes (e.g. login/logout in a other browser tab).
 authenticated.$subscribe((mutation, state) => {
   if (state.value && !sessionId.value) {
     sessionId.checkSession().then((sessionId) => {

+ 15 - 1
app/frontend/apps/mobile/views/Home.vue

@@ -7,7 +7,11 @@
     <br />
     <p v-on:click="logout">{{ i18n.t('Logout') }}</p>
     <br />
+    <p v-on:click="goToTickets">Go to Tickets</p>
+    <br />
     <p v-on:click="refetchConfig">refetchConfig</p>
+    <br />
+    <p v-on:click="fetchCurrentUser">fetchCurrentUser</p>
     <br /><br />
     <h1 class="text-lg mb-4">Configs:</h1>
     <template v-if="config.value">
@@ -26,6 +30,7 @@ import useSessionUserStore from '@common/stores/session/user'
 import { storeToRefs } from 'pinia'
 import { useRouter } from 'vue-router'
 import useApplicationConfigStore from '@common/stores/application/config'
+import { useCurrentUserQuery } from '@common/graphql/api'
 
 // TODO: Only testing for the notifications...
 const { notify } = useNotifications()
@@ -45,7 +50,7 @@ const router = useRouter()
 
 const logout = (): void => {
   authenticated.logout().then(() => {
-    router.push('/login')
+    router.push('login')
   })
 }
 
@@ -54,4 +59,13 @@ const config = useApplicationConfigStore()
 const refetchConfig = async (): Promise<void> => {
   await config.getConfig()
 }
+
+const fetchCurrentUser = () => {
+  const { result } = useCurrentUserQuery({ fetchPolicy: 'no-cache' })
+  console.log('result', result)
+}
+
+const goToTickets = () => {
+  router.push('/tickets')
+}
 </script>

+ 18 - 0
app/frontend/apps/mobile/views/Login.vue

@@ -85,10 +85,28 @@
 </template>
 
 <script setup lang="ts">
+import useNotifications from '@common/composables/useNotifications'
 import useApplicationConfigStore from '@common/stores/application/config'
 import useAuthenticationStore from '@common/stores/authenticated'
 import { useRouter } from 'vue-router'
 
+interface Props {
+  invalidatedSession?: string
+}
+
+const props = defineProps<Props>()
+
+// Output a hint, when the session is longer valid, maybe because because the session
+// was deleted on the server.
+if (props.invalidatedSession === '1') {
+  const { notify } = useNotifications()
+
+  notify({
+    message: __('The session is no longer valid. Please log in again.'),
+    type: 'warning',
+  })
+}
+
 const authentication = useAuthenticationStore()
 const loginFormValues = {
   login: '',

+ 8 - 1
app/frontend/apps/mobile/views/TicketOverview.vue

@@ -3,9 +3,16 @@
 <template>
   <div>
     <h1>{{ i18n.t('Ticket Overview') }}</h1>
+    <p v-on:click="goTo">Go to link Home</p>
   </div>
 </template>
 
 <script setup lang="ts">
-// TODO ...
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+
+const goTo = () => {
+  router.push('/')
+}
 </script>

+ 5 - 3
app/frontend/common/server/apollo/link/error.ts

@@ -7,6 +7,7 @@ import {
   GraphQLErrorExtensionsHandler,
   GraphQLErrorTypes,
 } from '@common/types/error'
+import useSessionIdStore from '@common/stores/session/id'
 
 const errorLink = onError(({ graphQLErrors, networkError, operation }) => {
   const errorContext = getErrorContext(operation)
@@ -31,9 +32,10 @@ const errorLink = onError(({ graphQLErrors, networkError, operation }) => {
         operation.operationName !== 'sessionId' &&
         type === GraphQLErrorTypes.NotAuthorized
       ) {
-        // Session Invalid:
-        // TODO ...do something...
-        log.warn('Session invalid...')
+        // Reset sessionId after an unathenticated error type.
+        useSessionIdStore().value = null
+
+        log.warn('Session invalid, trigger logout and show login page.')
       }
     })
   }

+ 13 - 7
app/frontend/common/stores/authenticated.ts

@@ -22,16 +22,21 @@ const useAuthenticatedStore = defineStore('authenticated', {
       const result = await logoutMutation.send()
 
       if (result?.logout?.success) {
-        await apolloClient.clearStore()
+        await this.clearAuthentication()
+      }
+    },
 
-        const sessionId = useSessionIdStore()
-        sessionId.value = null
+    async clearAuthentication(): Promise<void> {
+      await apolloClient.clearStore()
 
-        // Refresh the config after logout, to have only the non authenticated version.
-        await useApplicationConfigStore().resetAndGetConfig()
+      const sessionId = useSessionIdStore()
+      sessionId.value = null
+      this.value = false
 
-        // TODO... check for other things which must be removed/cleared during a logout.
-      }
+      // Refresh the config after logout, to have only the non authenticated version.
+      await useApplicationConfigStore().resetAndGetConfig()
+
+      // TODO... check for other things which must be removed/cleared during a logout.
     },
 
     async login(login: string, password: string): Promise<void> {
@@ -52,6 +57,7 @@ const useAuthenticatedStore = defineStore('authenticated', {
       if (newSessionId) {
         const sessionId = useSessionIdStore()
         sessionId.value = newSessionId
+        this.value = true
 
         await Promise.all([
           useApplicationConfigStore().getConfig(),

+ 3 - 1
app/frontend/common/types/error.ts

@@ -5,10 +5,12 @@ import { GraphQLErrorExtensions } from 'graphql'
 export enum GraphQLErrorTypes {
   UnkownError = 'Exceptions::UnkownError',
   NetworkError = 'Exceptions::NetworkError',
+
+  // This exception actually means 'NotAuthenticated'
   NotAuthorized = 'Exceptions::NotAuthorized',
 }
 
-export type GraphQLErrorTypeKeys = keyof GraphQLErrorTypes | 'test'
+export type GraphQLErrorTypeKeys = keyof GraphQLErrorTypes
 
 export interface GraphQLErrorExtensionsHandler {
   type: GraphQLErrorTypes

+ 5 - 1
config/initializers/content_security_policy.rb

@@ -50,8 +50,12 @@ Rails.application.config.content_security_policy do |policy|
   policy.frame_src   'www.youtube.com', 'player.vimeo.com'
 
   if Rails.env.development?
+    websocket_uri = proc do
+      "ws://localhost:#{Setting.get('websocket_port')}"
+    end
+
     policy.script_src  :self, :unsafe_eval, :unsafe_inline
-    policy.connect_src :self, :https, "http://#{ViteRuby.config.host_with_port}", "ws://#{ViteRuby.config.host_with_port}"
+    policy.connect_src :self, :https, "http://#{ViteRuby.config.host_with_port}", "ws://#{ViteRuby.config.host_with_port}", websocket_uri
   end
 end
 

+ 4 - 0
i18n/zammad.pot

@@ -9048,6 +9048,10 @@ msgstr ""
 msgid "The server settings could not be automatically detected. Please configure them manually."
 msgstr ""
 
+#: app/frontend/apps/mobile/views/Login.vue
+msgid "The session is no longer valid. Please log in again."
+msgstr ""
+
 #: app/models/store/file.rb
 msgid "The setting 'storage_provider' was not configured."
 msgstr ""