first working login and editing of file. bugg is för rendering with tiptap
This commit is contained in:
8868
frontend/package-lock.json
generated
Normal file
8868
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
135
frontend/src/components/FolderPicker.vue
Normal file
135
frontend/src/components/FolderPicker.vue
Normal 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>
|
||||
@@ -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 }
|
||||
})
|
||||
|
||||
|
||||
178
frontend/src/views/AdminView.vue
Normal file
178
frontend/src/views/AdminView.vue
Normal 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>
|
||||
84
frontend/src/views/LoginView.vue
Normal file
84
frontend/src/views/LoginView.vue
Normal 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>
|
||||
Reference in New Issue
Block a user