Consistent dialog styling widgets and using Paskia's shared backdrop. Internal password auth mimics Paskia. API paths changed (/auth goes to internal or paskia depending on config). All API calls and previews get access checks.

This commit is contained in:
Leo Vasanko
2026-01-30 18:28:26 +00:00
parent fa82fee53e
commit 14f2177514
23 changed files with 1109 additions and 386 deletions
+2 -1
View File
@@ -4,6 +4,7 @@
<script setup lang="ts">
import { useMainStore } from '@/stores/main'
import { apiFetch } from '@/repositories/Client'
import type { SelectedItems } from '@/repositories/Document'
import { reactive } from 'vue';
@@ -96,7 +97,7 @@ const filesystemdl = async (sel: SelectedItems, handle: FileSystemDirectoryHandl
const writable = await fileHandle.createWritable()
const url = `/files/${rel}`
console.log('Fetching', url)
const res = await fetch(url)
const res = await apiFetch(url)
if (!res.ok) {
store.error = `Failed to download ${url}: ${res.status} ${res.statusText}`
throw new Error(`Failed to download ${url}: ${res.status} ${res.statusText}`)
+46 -5
View File
@@ -30,14 +30,27 @@
<script setup lang="ts">
import { useMainStore } from '@/stores/main'
import { ref, nextTick, watchEffect } from 'vue'
import { useSsoAuthStore } from '@/stores/ssoAuth'
import { ref, nextTick, watchEffect, computed } from 'vue'
import ContextMenu from '@imengyu/vue3-context-menu'
import { showAuthIframe } from 'paskia'
import { resumeWatching } from '@/repositories/WS'
import router from '@/router';
const store = useMainStore()
const ssoStore = useSsoAuthStore()
const showSearchInput = ref<boolean>(false)
const search = ref<HTMLInputElement | null>()
const searchButton = ref<HTMLButtonElement | null>()
// Display name for SSO users
const displayUserName = computed(() => {
if (ssoStore.isExternalAuth && ssoStore.userName) {
return ssoStore.userName
}
return store.user.username
})
const props = defineProps<{
path: Array<string>
query: string
@@ -73,14 +86,42 @@ watchEffect(() => {
const settingsMenu = (e: Event) => {
// show the context menu
const items = []
items.push({ label: 'Change Password', onClick: () => { store.dialog = 'settings' }})
// For external auth, show user name as link to /auth/
if (ssoStore.isExternalAuth && store.user.isLoggedIn) {
items.push({
label: displayUserName.value || 'User Account',
onClick: () => { window.location.href = '/auth/' }
})
items.push({ divided: true })
}
// Only show password change for non-SSO users
if (!ssoStore.isExternalAuth) {
items.push({ label: 'Change Password', onClick: () => { store.dialog = 'settings' }})
}
if (store.user.privileged) {
items.push({ label: 'Admin Settings', onClick: () => { store.dialog = 'usermgmt' }})
}
if (store.user.isLoggedIn) {
items.push({ label: `Logout ${store.user.username ?? ''}`, onClick: () => store.logout() })
} else {
items.push({ label: 'Login', onClick: () => store.loginDialog() })
if (ssoStore.isExternalAuth) {
// For SSO, link to auth logout
items.push({ label: 'Logout', onClick: () => { window.location.href = '/auth/' }})
} else {
items.push({ label: `Logout ${store.user.username ?? ''}`, onClick: () => store.logout() })
}
} else if (!ssoStore.isExternalAuth) {
// Show login in paskia iframe overlay
items.push({ label: 'Login', onClick: async () => {
try {
await showAuthIframe('/auth/api/restricted')
resumeWatching()
} catch (e) {
console.log('Login cancelled')
}
}})
}
ContextMenu.showContextMenu({
// @ts-ignore
-101
View File
@@ -1,101 +0,0 @@
<template>
<ModalDialog name="login" title="Authentication required">
<form @submit.prevent="login">
<div class="login-container">
<label for="username">Username:</label>
<input
id="username"
name="username"
autocomplete="username"
spellcheck="false"
autocorrect="off"
required
v-model="loginForm.username"
/>
<label for="password">Password:</label>
<input
id="password"
name="password"
type="password"
autocomplete="current-password"
spellcheck="false"
autocorrect="off"
required
v-model="loginForm.password"
/>
</div>
<h3 class="error-text">
{{ loginForm.error || '\u00A0' }}
</h3>
<div class="dialog-buttons">
<div class="spacer"></div>
<input id="submit" type="submit" value="Login" class="button-login" />
</div>
</form>
</ModalDialog>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue'
import { loginUser } from '@/repositories/User'
import type { ISimpleError } from '@/repositories/Client'
import { useMainStore } from '@/stores/main'
const confirmLoading = ref<boolean>(false)
const store = useMainStore()
const loginForm = reactive({
username: '',
password: '',
error: ''
})
const login = async () => {
try {
loginForm.error = ''
confirmLoading.value = true
const msg = await loginUser(loginForm.username, loginForm.password)
store.login(msg.data.username, !!msg.data.privileged)
} catch (error) {
const httpError = error as ISimpleError
loginForm.error = httpError.message || '🛑 Unknown error'
} finally {
confirmLoading.value = false
}
}
</script>
<style scoped>
.login-container {
display: grid;
gap: 1rem;
grid-template-columns: 1fr 2fr;
justify-content: center;
align-items: center;
margin: 1rem 0;
}
.dialog-buttons {
display: flex;
justify-content: space-between;
align-items: center;
}
.button-login {
color: #fff;
background: var(--soft-color);
cursor: pointer;
font-weight: bold;
border: 0;
border-radius: .5rem;
padding: .5rem 2rem;
margin-left: auto;
transition: all var(--transition-time) linear;
}
.button-login:hover, .button-login:focus {
background: var(--accent-color);
box-shadow: 0 0 .3rem #000;
}
.error-text {
color: var(--red-color);
height: 1em;
}
</style>
+206 -31
View File
@@ -13,6 +13,7 @@
<script setup lang="ts">
import { ref, onMounted, watchEffect, nextTick } from 'vue'
import { useMainStore } from '@/stores/main'
import { holdGlobalBackdrop, releaseGlobalBackdrop } from 'paskia'
const dialog = ref<HTMLDialogElement | null>(null)
const store = useMainStore()
@@ -20,6 +21,7 @@ const store = useMainStore()
const close = () => {
dialog.value!.close()
store.dialog = ''
releaseGlobalBackdrop()
}
const props = defineProps<{
@@ -29,6 +31,7 @@ const props = defineProps<{
const show = () => {
store.dialog = props.name
holdGlobalBackdrop()
setTimeout(() => {
dialog.value!.showModal()
nextTick(() => {
@@ -44,47 +47,219 @@ watchEffect(() => {
</script>
<style>
/* Style for the background */
/* ===========================================
DIALOG GLOBAL STYLES
Shared styling for all modal dialogs.
Login page (auth.py) has matching CSS.
=========================================== */
dialog::backdrop {
content: '';
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #0008;
backdrop-filter: blur(0.4em);
z-index: 1000;
display: none;
}
/* Hide the dialog by default */
/* Dialog container */
dialog[open] {
background: #ddd;
color: black;
display: block;
color: #000;
border: none;
font-size: 1.2rem;
border-radius: 0.5rem;
box-shadow: 0.2rem 0.2rem 1rem #000;
padding: 1rem;
box-shadow: 0 0 1rem #0008;
padding: 0;
position: fixed;
top: 0;
left: 0;
z-index: 1001;
}
input {
font: inherit;
}
dialog[open] > h1 {
background: var(--soft-color);
color: #fff;
font-size: 1.2rem;
margin: -1rem -1rem 0 -1rem;
padding: 0.5rem 1rem 0.5rem 1rem;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1100;
max-width: 90vw;
max-height: 90vh;
overflow: auto;
font-size: 1rem;
}
/* Dialog title bar */
dialog[open] > h1 {
background: #146;
color: #fff;
font-size: 1.2rem;
font-weight: normal;
margin: 0;
padding: 0.5rem 1rem;
position: sticky;
top: 0;
}
/* Dialog content area */
dialog[open] > div {
padding: 1em 0;
padding: 1rem;
}
/* Section headings inside dialog */
dialog h3 {
font-size: 1rem;
font-weight: 600;
margin: 1rem 0 0.5rem 0;
}
dialog h3:first-child {
margin-top: 0;
}
/* Form inputs */
dialog input[type="text"],
dialog input[type="password"],
dialog select {
font: inherit;
font-size: 1rem;
padding: 0.5rem;
border: 2px solid #888;
border-radius: 0.25rem;
background: #fff;
color: #000;
min-width: 12rem;
}
dialog input[type="text"]:focus,
dialog input[type="password"]:focus,
dialog select:focus {
outline: none;
border-color: #f80;
}
/* Labels */
dialog label {
font-size: 1rem;
}
/* Buttons */
dialog button,
dialog input[type="submit"],
dialog input[type="reset"],
dialog .button {
font: inherit;
font-size: 1rem;
padding: 0.5rem 1rem;
background: #146;
color: #fff;
border: none;
border-radius: 0.25rem;
cursor: pointer;
}
dialog button:hover,
dialog input[type="submit"]:hover,
dialog input[type="reset"]:hover,
dialog .button:hover {
background: #f80;
}
dialog button:disabled,
dialog input[type="submit"]:disabled,
dialog input[type="reset"]:disabled,
dialog .button:disabled {
background: #888;
cursor: not-allowed;
}
/* Small button variant */
dialog .button.small {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
/* Danger button variant */
dialog .button.danger {
background: #c00;
}
dialog .button.danger:hover:not(:disabled) {
background: #f00;
}
/* Form row layout (label + input side by side) */
dialog .form-row {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.5rem 1rem;
align-items: center;
margin-bottom: 0.5rem;
}
/* Form grid for multiple label+input pairs */
dialog .form-grid {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.5rem 1rem;
align-items: center;
}
/* Dialog button row (footer) */
dialog .dialog-buttons {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 0.5rem;
margin-top: 1rem;
}
/* Error text */
dialog .error-text {
color: #c00;
font-size: 0.875rem;
min-height: 1.2em;
margin: 0.5rem 0;
}
/* Success message */
dialog .success-message {
background: #f80;
color: #000;
padding: 0.5rem;
border-radius: 0.25rem;
margin: 0.5rem 0;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
}
/* Data tables inside dialogs */
dialog table {
width: 100%;
border-collapse: collapse;
margin: 0.5rem 0;
font-size: 1rem;
}
dialog th,
dialog td {
border: 1px solid #888;
padding: 0.5rem;
text-align: left;
}
dialog th {
background: #146;
color: #fff;
font-weight: normal;
}
dialog td {
background: #fff;
}
/* Checkbox alignment in tables */
dialog td input[type="checkbox"] {
margin: 0;
}
/* Paragraph text */
dialog p {
margin: 0 0 0.5rem 0;
font-size: 1rem;
}
/* Loading state */
dialog .loading {
padding: 2rem;
text-align: center;
color: #666;
}
</style>
+5 -36
View File
@@ -3,8 +3,8 @@
<form>
<template v-if="store.user.isLoggedIn">
<h3>Update your authentication</h3>
<div class="login-container">
<label for="username">New password:</label>
<div class="form-grid">
<label for="passwordChange">New password:</label>
<input
ref="passwordChange"
id="passwordChange"
@@ -26,9 +26,9 @@
v-model="form.password"
/>
</div>
<h3 class="error-text">
<p class="error-text">
{{ form.error || '\u00A0' }}
</h3>
</p>
<div class="dialog-buttons">
<input id="close" type="reset" value="Close" class="button" @click=close />
<div class="spacer"></div>
@@ -92,36 +92,5 @@ const submit = async (ev: Event) => {
</script>
<style scoped>
.login-container {
display: grid;
gap: 1rem;
grid-template-columns: 1fr 2fr;
justify-content: center;
align-items: center;
margin: 1rem 0;
}
.dialog-buttons {
display: flex;
justify-content: space-between;
align-items: center;
}
.button-login {
color: #fff;
background: var(--soft-color);
cursor: pointer;
font-weight: bold;
border: 0;
border-radius: .5rem;
padding: .5rem 2rem;
margin-left: auto;
transition: all var(--transition-time) linear;
}
.button-login:hover, .button-login:focus {
background: var(--accent-color);
box-shadow: 0 0 .3rem #000;
}
.error-text {
color: var(--red-color);
height: 1em;
}
/* Component-specific styles - most styling comes from ModalDialog.vue global styles */
</style>
+22 -60
View File
@@ -4,21 +4,25 @@
<div v-else>
<h3>Server Settings</h3>
<div class="form-row">
<input
id="publicServer"
type="checkbox"
v-model="serverSettings.public"
<label for="authMode">Authentication:</label>
<select
id="authMode"
v-model="serverSettings.authentication"
@change="updateServerSettings"
/>
<label for="publicServer">Publicly accessible without any user account.</label>
>
<option value="password">Password (built-in users)</option>
<option value="paskia">Paskia (external SSO)</option>
<option value="none">None (public access)</option>
</select>
</div>
<template v-if="serverSettings.authentication === 'password'">
<h3>Users</h3>
<button @click="addUser" class="button" title="Add new user"> Add User</button>
<div v-if="success" class="success-message" @click="copySuccess(false)">
{{ success }}
<button v-if="success.includes('Password:') || success.includes('New password:')" @click.stop="copySuccess(true)" class="button small" title="Copy to clipboard">{{ copyButtonText }}</button>
</div>
<table class="user-table">
<table>
<thead>
<tr>
<th>Username</th>
@@ -45,7 +49,8 @@
</tr>
</tbody>
</table>
<h3 class="error-text">{{ error || '\u00A0' }}</h3>
</template>
<p class="error-text">{{ error || '\u00A0' }}</p>
<div class="dialog-buttons">
<button @click="close" class="button">Close</button>
</div>
@@ -55,7 +60,7 @@
<script lang="ts" setup>
import { ref, reactive, onMounted, watch } from 'vue'
import { listUsers, createUser, updateUser, deleteUser, updatePublic } from '@/repositories/User'
import { listUsers, createUser, updateUser, deleteUser, updateAuthentication, type AuthMode } from '@/repositories/User'
import type { ISimpleError } from '@/repositories/Client'
import { useMainStore } from '@/stores/main'
@@ -72,7 +77,7 @@ const error = ref('')
const success = ref('')
const copyButtonText = ref('📋')
const serverSettings = reactive({
public: false
authentication: 'password' as AuthMode
})
const close = () => {
@@ -197,9 +202,9 @@ const updateServerSettings = async () => {
try {
error.value = ''
success.value = ''
await updatePublic(serverSettings.public)
await updateAuthentication(serverSettings.authentication)
// Update store
store.server.public = serverSettings.public
store.server.authentication = serverSettings.authentication
success.value = 'Server settings updated'
} catch (e) {
const httpError = e as ISimpleError
@@ -208,58 +213,15 @@ const updateServerSettings = async () => {
}
onMounted(() => {
serverSettings.public = store.server.public
serverSettings.authentication = store.server.authentication || 'password'
loadUsers()
})
watch(() => store.server.public, (newVal) => {
serverSettings.public = newVal
watch(() => store.server.authentication, (newVal) => {
serverSettings.authentication = newVal || 'password'
})
</script>
<style scoped>
.user-table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
.user-table th, .user-table td {
border: 1px solid var(--border-color);
padding: 0.5rem;
text-align: left;
}
.user-table th {
background: var(--soft-color);
}
.button.small {
padding: 0.25rem 0.5rem;
font-size: 0.8rem;
margin-right: 0.25rem;
}
.button.danger {
background: var(--red-color);
color: white;
}
.button.danger:hover {
background: #d00;
}
.form-row {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 0.5rem;
}
.form-row label {
min-width: 100px;
}
.success-message {
background: var(--accent-color);
color: white;
padding: 0.5rem;
border-radius: 0.25rem;
margin-top: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
</style>
/* Component-specific styles - most styling comes from ModalDialog.vue global styles */
</style>