first working login and editing of file. bugg is för rendering with tiptap

This commit is contained in:
Björn Blomberg
2026-04-10 19:56:21 +02:00
parent 41dc1d9988
commit 05b773c14c
14 changed files with 9459 additions and 24 deletions

View File

8868
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,135 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { gql } from '@/lib/gql'
const props = defineProps<{
modelValue: string
label?: string
placeholder?: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const open = ref(false)
const currentPath = ref(props.modelValue)
const directories = ref<{ name: string; path: string }[]>([])
const loading = ref(false)
async function fetchDirectories(path: string) {
loading.value = true
try {
const data = await gql<{ serverDirectories: { name: string; path: string }[] }>(
`query Dirs($p: String) { serverDirectories(path: $p) { name path } }`,
{ p: path || null }
)
directories.value = data.serverDirectories
// Update input box to the fetched path if we passed empty and it resolved to root
if (directories.value.length > 0 && path === '') {
// Find parent or use current
const parent = directories.value.find(d => d.name === '..')
if (parent) currentPath.value = parent.path + '/.' // hacky, but fine
}
} catch (err) {
console.error(err)
} finally {
loading.value = false
}
}
function handleOpen() {
open.value = true
currentPath.value = props.modelValue
fetchDirectories(currentPath.value)
}
function selectPath() {
emit('update:modelValue', currentPath.value)
open.value = false
}
function navigate(toPath: string) {
currentPath.value = toPath
fetchDirectories(toPath)
}
// Ensure local currentPath updates when props change externally
watch(() => props.modelValue, (val) => {
currentPath.value = val
})
</script>
<template>
<div class="relative">
<label v-if="label" class="label">{{ label }}</label>
<div class="flex gap-2">
<input
:value="modelValue"
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
class="input flex-1"
:placeholder="placeholder"
/>
<button type="button" class="btn-secondary" @click="handleOpen">Browse...</button>
</div>
<!-- Modal Modal -->
<transition name="fade">
<div v-if="open" class="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/50 backdrop-blur-sm p-4">
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-xl border border-slate-200 dark:border-slate-700 w-full max-w-lg overflow-hidden flex flex-col max-h-[80vh]">
<!-- Header -->
<div class="p-4 border-b border-slate-200 dark:border-slate-700 flex items-center justify-between">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">Select Folder</h3>
<button type="button" @click="open = false" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
<!-- Body -->
<div class="p-4 bg-slate-50 dark:bg-slate-900/50 flex-1 overflow-auto flex flex-col gap-3">
<input
v-model="currentPath"
@keydown.enter.prevent="fetchDirectories(currentPath)"
class="input font-mono text-sm"
placeholder="Path..."
/>
<div class="flex-1 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg overflow-y-auto min-h-[200px]">
<div v-if="loading" class="p-4 text-center text-slate-400 animate-pulse text-sm">
Loading...
</div>
<ul v-else class="divide-y divide-slate-100 dark:divide-slate-700/50">
<li v-if="directories.length === 0" class="p-3 text-sm text-slate-500 text-center">
No subdirectories
</li>
<li v-for="dir in directories" :key="dir.path">
<button
type="button"
class="w-fulltext-left flex items-center gap-2 px-3 py-2 w-full hover:bg-accent-50 dark:hover:bg-accent-500/10 focus:bg-accent-50 focus:outline-none transition group"
@click="navigate(dir.path)"
>
<svg class="w-4 h-4 text-accent-500 group-hover:text-accent-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
<span class="text-sm font-medium text-slate-700 dark:text-slate-300 truncate">{{ dir.name }}</span>
</button>
</li>
</ul>
</div>
</div>
<!-- Footer -->
<div class="p-4 border-t border-slate-200 dark:border-slate-700 flex justify-end gap-2 bg-white dark:bg-slate-800">
<button type="button" class="btn-ghost" @click="open = false">Cancel</button>
<button type="button" class="btn-primary" @click="selectPath">Select Reference</button>
</div>
</div>
</div>
</transition>
</div>
</template>
<style scoped>
.fade-enter-active, .fade-leave-active { transition: opacity 0.15s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>

View File

@@ -23,7 +23,7 @@ export const useAppStore = defineStore('app', () => {
)
token.value = data.login
username.value = user
localStorage.setItem('token', data.login)
localStorage.setItem('token', data.login); console.log('[login] token is:', data.login, localStorage.getItem('token'))
localStorage.setItem('username', user)
}
@@ -41,3 +41,4 @@ export const useAppStore = defineStore('app', () => {
return { requiresSetup, token, username, checkStatus, login, logout }
})

View File

@@ -0,0 +1,178 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { gql } from '@/lib/gql'
import FolderPicker from '@/components/FolderPicker.vue'
const storagePath = ref('')
const ldapEnabled = ref(false)
const ldapHost = ref('')
const ldapPort = ref(389)
const ldapBaseDN = ref('')
const ldapBindDN = ref('')
const ldapBindPassword = ref('')
const oldPass = ref('')
const newPass = ref('')
const loading = ref(true)
const status = ref({ msg: '', isError: false })
onMounted(async () => {
try {
const data = await gql<{ config: any }>(`{ config { storagePath ldap { host port baseDN bindDN } } }`)
storagePath.value = data.config.storagePath || ''
if (data.config.ldap) {
ldapEnabled.value = true
ldapHost.value = data.config.ldap.host
ldapPort.value = data.config.ldap.port
ldapBaseDN.value = data.config.ldap.baseDN
ldapBindDN.value = data.config.ldap.bindDN
}
} catch (err: any) {
status.value = { msg: err.message, isError: true }
} finally {
loading.value = false
}
})
function showStatus(msg: string, isError: boolean = false) {
status.value = { msg, isError }
setTimeout(() => status.value.msg = '', 4000)
}
async function saveStorage() {
try {
await gql(`mutation UpdateStorage($path: String!) { updateStoragePath(path: $path) }`, { path: storagePath.value })
showStatus('Storage path updated.')
} catch (err: any) {
showStatus(err.message, true)
}
}
async function saveLdap() {
try {
let input = null
if (ldapEnabled.value) {
input = {
host: ldapHost.value,
port: ldapPort.value,
baseDN: ldapBaseDN.value,
bindDN: ldapBindDN.value,
bindPassword: ldapBindPassword.value
}
}
await gql(`mutation UpdateLdap($i: LDAPInput) { updateLdapConfig(input: $i) }`, { i: input })
showStatus(ldapEnabled.value ? 'LDAP config updated.' : 'LDAP disabled.')
} catch (err: any) {
showStatus(err.message, true)
}
}
async function savePassword() {
if (newPass.value.length < 8) {
showStatus('New password must be at least 8 characters.', true)
return
}
try {
await gql(`mutation ChangePass($old: String!, $new: String!) { changePassword(old: $old, new: $new) }`, { old: oldPass.value, new: newPass.value })
showStatus('Password updated successfully.')
oldPass.value = ''
newPass.value = ''
} catch (err: any) {
showStatus(err.message, true)
}
}
</script>
<template>
<div class="p-6 max-w-2xl mx-auto space-y-8">
<h1 class="text-3xl font-bold mb-6">Admin Settings</h1>
<div v-if="status.msg" :class="[
'px-4 py-3 rounded-lg text-sm border',
status.isError ? 'bg-red-50 dark:bg-red-500/10 text-red-600 dark:text-red-400 border-red-200 dark:border-red-500/20' : 'bg-green-50 dark:bg-green-500/10 text-green-600 dark:text-green-400 border-green-200 dark:border-green-500/20'
]">
{{ status.msg }}
</div>
<!-- Storage Setup -->
<section class="card p-6">
<h2 class="text-lg font-semibold mb-4 border-b border-slate-200 dark:border-slate-700 pb-2">Storage Path</h2>
<div class="space-y-4">
<div>
<label class="label">Document Path</label>
<p class="text-xs text-slate-500 mb-2">Where Archivum reads and writes .adoc files</p>
<div class="flex gap-2">
<FolderPicker v-model="storagePath" class="flex-1" placeholder="/var/data/wiki" />
<button class="btn-primary flex-none" @click="saveStorage">Save</button>
</div>
</div>
</div>
</section>
<!-- Admin Account -->
<section class="card p-6">
<h2 class="text-lg font-semibold mb-4 border-b border-slate-200 dark:border-slate-700 pb-2">Admin Account</h2>
<div class="grid gap-4 sm:grid-cols-2">
<div class="sm:col-span-2">
<p class="text-sm text-slate-600 dark:text-slate-400 mb-2">Change your password (requires current password).</p>
</div>
<div>
<label class="label">Current Password</label>
<input v-model="oldPass" type="password" class="input" placeholder="••••••••" />
</div>
<div>
<label class="label">New Password</label>
<input v-model="newPass" type="password" class="input" placeholder="••••••••" />
</div>
<div class="sm:col-span-2 flex justify-end">
<button class="btn-primary" @click="savePassword">Change Password</button>
</div>
</div>
</section>
<!-- LDAP Setup -->
<section class="card p-6">
<div class="flex items-center justify-between border-b border-slate-200 dark:border-slate-700 pb-2 mb-4">
<h2 class="text-lg font-semibold">LDAP Integration</h2>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="ldapEnabled" class="rounded border-slate-300 text-accent-600 focus:ring-accent-500" />
<span class="text-sm font-medium">Enable LDAP</span>
</label>
</div>
<div v-if="ldapEnabled" class="grid gap-4 sm:grid-cols-2 mb-4">
<div class="sm:col-span-2 md:col-span-1">
<label class="label">Host</label>
<input v-model="ldapHost" class="input" placeholder="ldap.example.com" />
</div>
<div class="sm:col-span-2 md:col-span-1">
<label class="label">Port</label>
<input v-model.number="ldapPort" type="number" class="input" placeholder="389" />
</div>
<div class="sm:col-span-2">
<label class="label">Base DN <span class="text-slate-400 text-xs font-normal">(e.g. dc=example,dc=com)</span></label>
<input v-model="ldapBaseDN" class="input" placeholder="dc=example,dc=com" />
</div>
<div class="sm:col-span-2 md:col-span-1">
<label class="label">Bind DN (Service Account)</label>
<input v-model="ldapBindDN" class="input" placeholder="cn=reader,dc=example,dc=com" />
</div>
<div class="sm:col-span-2 md:col-span-1">
<label class="label">Bind Password</label>
<input v-model="ldapBindPassword" type="password" class="input" />
</div>
</div>
<div class="flex justify-end">
<button class="btn-primary" @click="saveLdap">
{{ ldapEnabled ? 'Save LDAP Config' : 'Save & Disable LDAP' }}
</button>
</div>
</section>
</div>
</template>

View File

@@ -0,0 +1,84 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useAppStore } from '@/stores/app'
const app = useAppStore()
const username = ref('')
const password = ref('')
const loading = ref(false)
const error = ref('')
async function submit() {
error.value = ''
loading.value = true
try {
await app.login(username.value, password.value)
} catch (err: any) {
if (err instanceof Error) {
error.value = err.message
} else {
error.value = 'Failed to sign in'
}
} finally {
loading.value = false
}
}
</script>
<template>
<div class="min-h-screen flex items-center justify-center bg-slate-50 dark:bg-slate-900 p-4">
<div class="w-full max-w-md bg-white dark:bg-slate-800 rounded-xl shadow-xl border border-slate-200 dark:border-slate-700 p-8 space-y-6">
<!-- Logo -->
<div class="flex items-center justify-center gap-2 font-bold text-slate-900 dark:text-white text-xl">
<svg class="w-8 h-8 text-accent-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
</svg>
Archivum
</div>
<h2 class="text-center text-slate-500 dark:text-slate-400">Sign in to continue</h2>
<form @submit.prevent="submit" class="space-y-4">
<div>
<label class="block text-sm font-medium mb-1 text-slate-700 dark:text-slate-300">Username</label>
<input
v-model="username"
type="text"
required
class="w-full px-3 py-2 bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-600 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-accent-500 text-slate-900 dark:text-white"
placeholder="admin"
/>
</div>
<div>
<label class="block text-sm font-medium mb-1 text-slate-700 dark:text-slate-300">Password</label>
<input
v-model="password"
type="password"
required
class="w-full px-3 py-2 bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-600 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-accent-500 text-slate-900 dark:text-white"
placeholder="••••••••"
/>
</div>
<div v-if="error" class="text-sm text-red-600 bg-red-50 dark:bg-red-500/10 dark:text-red-400 p-3 rounded-lg border border-red-200 dark:border-red-500/20">
{{ error }}
</div>
<button
type="submit"
class="w-full flex justify-center py-2.5 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-accent-600 hover:bg-accent-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-500 disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="loading"
>
<svg v-if="loading" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"/>
</svg>
{{ loading ? 'Signing in...' : 'Sign in' }}
</button>
</form>
</div>
</div>
</template>