Selaa lähdekoodia

fix: User Information panel and login/logout flow

jamesread 8 kuukautta sitten
vanhempi
commit
8686a5629e

+ 36 - 6
frontend/resources/vue/App.vue

@@ -7,12 +7,11 @@
         </template>
         </template>
 
 
         <template #user-info>
         <template #user-info>
-            <div class="flex-row" style="gap: .5em;">
+            <div class="flex-row user-info" style="gap: .5em;">
                 <span id="link-login" v-if="!isLoggedIn"><router-link to="/login">Login</router-link></span>
                 <span id="link-login" v-if="!isLoggedIn"><router-link to="/login">Login</router-link></span>
-                <div v-else>
-                    <span id="username-text" :title="'Provider: ' + userProvider">{{ username }}</span>
-                    <span id="link-logout" v-if="isLoggedIn"><a href="/api/Logout">Logout</a></span>
-                </div>
+                <router-link v-else to="/user" class="user-link">
+                    <span id="username-text">{{ username }}</span>
+                </router-link>
                 <HugeiconsIcon :icon="UserCircle02Icon" width = "1.5em" height = "1.5em" />
                 <HugeiconsIcon :icon="UserCircle02Icon" width = "1.5em" height = "1.5em" />
             </div>
             </div>
 
 
@@ -70,7 +69,6 @@ import logoUrl from '../../OliveTinLogo.png';
 
 
 const sidebar = ref(null);
 const sidebar = ref(null);
 const username = ref('guest');
 const username = ref('guest');
-const userProvider = ref('system');
 const isLoggedIn = ref(false);
 const isLoggedIn = ref(false);
 const serverConnection = ref('Connected');
 const serverConnection = ref('Connected');
 const currentVersion = ref('?');
 const currentVersion = ref('?');
@@ -88,6 +86,23 @@ function toggleSidebar() {
     sidebar.value.toggle()
     sidebar.value.toggle()
 }
 }
 
 
+function updateHeaderFromInit() {
+    if (window.initResponse) {
+        username.value = window.initResponse.authenticatedUser
+        isLoggedIn.value = window.initResponse.authenticatedUser !== '' && window.initResponse.authenticatedUser !== 'guest'
+        currentVersion.value = window.initResponse.currentVersion
+        bannerMessage.value = window.initResponse.bannerMessage || ''
+        bannerCss.value = window.initResponse.bannerCss || ''
+        showFooter.value = window.initResponse.showFooter
+        showNavigation.value = window.initResponse.showNavigation
+        showLogs.value = window.initResponse.showLogList
+        showDiagnostics.value = window.initResponse.showDiagnostics
+    }
+}
+
+// Export the function to window so other components can call it
+window.updateHeaderFromInit = updateHeaderFromInit
+
 async function requestInit() {
 async function requestInit() {
     try {
     try {
         const initResponse = await window.client.init({})
         const initResponse = await window.client.init({})
@@ -164,3 +179,18 @@ onMounted(() => {
     requestInit()
     requestInit()
 })
 })
 </script>
 </script>
+
+<style scoped>
+.user-info span {
+    margin-left: 1em;
+}
+
+.user-link {
+    text-decoration: none;
+    color: inherit;
+}
+
+.user-link:hover {
+    text-decoration: underline;
+}
+</style>

+ 6 - 0
frontend/resources/vue/router.js

@@ -85,6 +85,12 @@ const routes = [
     component: () => import('./views/LoginView.vue'),
     component: () => import('./views/LoginView.vue'),
     meta: { title: 'Login' }
     meta: { title: 'Login' }
   },
   },
+  {
+    path: '/user',
+    name: 'UserInformation',
+    component: () => import('./views/UserControlPanel.vue'),
+    meta: { title: 'User Information' }
+  },
   {
   {
     path: '/:pathMatch(.*)*',
     path: '/:pathMatch(.*)*',
     name: 'NotFound',
     name: 'NotFound',

+ 5 - 0
frontend/resources/vue/views/LoginView.vue

@@ -83,6 +83,11 @@ async function handleLocalLogin() {
         window.initError = false
         window.initError = false
         window.initErrorMessage = ''
         window.initErrorMessage = ''
         window.initCompleted = true
         window.initCompleted = true
+        
+        // Update the header with new user info
+        if (window.updateHeaderFromInit) {
+          window.updateHeaderFromInit()
+        }
       } catch (initErr) {
       } catch (initErr) {
         console.error('Failed to reinitialize after login:', initErr)
         console.error('Failed to reinitialize after login:', initErr)
       }
       }

+ 150 - 0
frontend/resources/vue/views/UserControlPanel.vue

@@ -0,0 +1,150 @@
+<template>
+  <Section title="User Information" class="small">
+    <div v-if="!isLoggedIn" class="user-not-logged-in">
+      <p>You are not currently logged in.</p>
+      <p>To access user settings and logout, please <router-link to="/login">log in</router-link>.</p>
+    </div>
+
+    <div v-else class="user-control-panel">
+      <dl class="user-info">
+        <dt>Username</dt>
+        <dd>{{ username }}</dd>
+        <dt v-if="userProvider !== 'system'">Provider</dt>
+        <dd v-if="userProvider !== 'system'">{{ userProvider }}</dd>
+        <dt v-if="usergroup">Group</dt>
+        <dd v-if="usergroup">{{ usergroup }}</dd>
+      </dl>
+
+      <div class="user-actions">
+        <div class="action-buttons">
+          <button @click="handleLogout" class="button bad" :disabled="loggingOut">
+            {{ loggingOut ? 'Logging out...' : 'Logout' }}
+          </button>
+        </div>
+      </div>
+    </div>
+  </Section>
+</template>
+
+<script setup>
+import { ref, onMounted, watch, onUnmounted } from 'vue'
+import { useRouter } from 'vue-router'
+import Section from 'picocrank/vue/components/Section.vue'
+
+const router = useRouter()
+
+const isLoggedIn = ref(false)
+const username = ref('guest')
+const userProvider = ref('system')
+const usergroup = ref('')
+const loggingOut = ref(false)
+
+function updateUserInfo() {
+  if (window.initResponse) {
+    isLoggedIn.value = window.initResponse.authenticatedUser !== '' && window.initResponse.authenticatedUser !== 'guest'
+    username.value = window.initResponse.authenticatedUser
+    userProvider.value = window.initResponse.authenticatedUserProvider || 'system'
+    usergroup.value = window.initResponse.effectivePolicy?.usergroup || ''
+  }
+}
+
+async function handleLogout() {
+  loggingOut.value = true
+  
+  try {
+    await window.client.logout({})
+    
+    // Re-initialize to get updated user context (should be guest)
+    try {
+      const initResponse = await window.client.init({})
+      window.initResponse = initResponse
+      window.initError = false
+      window.initErrorMessage = ''
+      window.initCompleted = true
+      
+      // Update the header with new user info
+      if (window.updateHeaderFromInit) {
+        window.updateHeaderFromInit()
+      }
+    } catch (initErr) {
+      console.error('Failed to reinitialize after logout:', initErr)
+    }
+    
+    // Redirect to home page
+    router.push('/')
+  } catch (err) {
+    console.error('Logout error:', err)
+  } finally {
+    loggingOut.value = false
+  }
+}
+
+let watchInterval = null
+
+onMounted(() => {
+  updateUserInfo()
+  
+  // Watch for changes to init response
+  watchInterval = setInterval(() => {
+    if (window.initResponse) {
+      updateUserInfo()
+    }
+  }, 1000)
+})
+
+onUnmounted(() => {
+  if (watchInterval) {
+    clearInterval(watchInterval)
+  }
+})
+</script>
+
+<style scoped>
+section {
+  margin: auto;
+}
+
+.user-not-logged-in {
+  padding: 2rem;
+  text-align: center;
+}
+
+.user-not-logged-in p {
+  margin: 1rem 0;
+}
+
+.user-control-panel {
+  display: grid;
+  grid-template-columns: 1fr;
+  gap: 2rem;
+}
+
+.action-buttons {
+  display: flex;
+  gap: 1rem;
+}
+
+.button {
+  padding: 0.75rem 1.5rem;
+  border-radius: 4px;
+  border: none;
+  cursor: pointer;
+  text-align: center;
+  font-weight: 500;
+  transition: background-color 0.2s;
+}
+
+.button.bad {
+  background-color: #dc3545;
+  color: white;
+}
+
+.button.bad:hover:not(:disabled) {
+  background-color: #c82333;
+}
+
+.button:disabled {
+  opacity: 0.6;
+  cursor: not-allowed;
+}
+</style>

+ 26 - 0
integration-tests/configs/localAuth/config.yaml

@@ -0,0 +1,26 @@
+#
+# Integration Test Config: Local User Authentication
+#
+
+listenAddressSingleHTTPFrontend: 0.0.0.0:1337
+
+logLevel: "DEBUG"
+checkForUpdates: false
+
+# Enable local user authentication
+authLocalUsers:
+  enabled: true
+  users:
+    - username: "testuser"
+      usergroup: "admin"
+      password: "testpass123"
+
+# Simple actions for testing
+actions:
+- title: Ping Google.com
+  shell: ping google.com -c 1
+  icon: ping
+
+- title: sleep 2 seconds
+  shell: sleep 2
+  icon: "&#x1F971"

+ 19 - 2
service/internal/api/api.go

@@ -328,9 +328,26 @@ func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *connect.Request[ap
 }
 }
 
 
 func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.LogoutRequest]) (*connect.Response[apiv1.LogoutResponse], error) {
 func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.LogoutRequest]) (*connect.Response[apiv1.LogoutResponse], error) {
-	// user := acl.UserFromContext(ctx, cfg)
+	user := acl.UserFromContext(ctx, req, api.cfg)
+
+	log.WithFields(log.Fields{
+		"username": user.Username,
+		"provider": user.Provider,
+	}).Info("Logout: User logged out")
+
+	response := connect.NewResponse(&apiv1.LogoutResponse{})
 
 
-	return nil, nil
+	// Clear the authentication cookie by setting it to expire
+	cookie := &http.Cookie{
+		Name:     "olivetin-sid-local",
+		Value:    "",
+		MaxAge:   -1, // This tells the browser to delete the cookie
+		HttpOnly: true,
+		Path:     "/",
+	}
+	response.Header().Set("Set-Cookie", cookie.String())
+
+	return response, nil
 }
 }
 
 
 func (api *oliveTinAPI) GetActionBinding(ctx ctx.Context, req *connect.Request[apiv1.GetActionBindingRequest]) (*connect.Response[apiv1.GetActionBindingResponse], error) {
 func (api *oliveTinAPI) GetActionBinding(ctx ctx.Context, req *connect.Request[apiv1.GetActionBindingRequest]) (*connect.Response[apiv1.GetActionBindingResponse], error) {