fix: User Information panel and login/logout flow

This commit is contained in:
jamesread 2025-10-26 13:42:06 +00:00
parent 43cfe41378
commit 8686a5629e
6 changed files with 242 additions and 8 deletions

View File

@ -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> <router-link v-else to="/user" class="user-link">
<span id="username-text" :title="'Provider: ' + userProvider">{{ username }}</span> <span id="username-text">{{ username }}</span>
<span id="link-logout" v-if="isLoggedIn"><a href="/api/Logout">Logout</a></span> </router-link>
</div>
<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>

View File

@ -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',

View File

@ -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)
} }

View File

@ -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>

View File

@ -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"

View File

@ -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)
return nil, nil log.WithFields(log.Fields{
"username": user.Username,
"provider": user.Provider,
}).Info("Logout: User logged out")
response := connect.NewResponse(&apiv1.LogoutResponse{})
// 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) {