diff --git a/frontend/components/connections/GroupEditDialog.vue b/frontend/components/connections/GroupEditDialog.vue new file mode 100644 index 0000000..333fdef --- /dev/null +++ b/frontend/components/connections/GroupEditDialog.vue @@ -0,0 +1,103 @@ + + + + + + + + Group Name * + + + + + + Parent Group + + + + + {{ error }} + + + + + + + + + + diff --git a/frontend/components/connections/HostCard.vue b/frontend/components/connections/HostCard.vue new file mode 100644 index 0000000..13fdb35 --- /dev/null +++ b/frontend/components/connections/HostCard.vue @@ -0,0 +1,97 @@ + + + + + + + + + + + {{ host.name }} + {{ host.hostname }}:{{ host.port }} + + + + {{ host.protocol.toUpperCase() }} + + + + + {{ host.group.name }} + Ungrouped + {{ formatLastConnected(host.lastConnectedAt) }} + + + + + {{ tag }} + + + + + Edit + Delete + + + diff --git a/frontend/components/connections/HostEditDialog.vue b/frontend/components/connections/HostEditDialog.vue new file mode 100644 index 0000000..f90c49b --- /dev/null +++ b/frontend/components/connections/HostEditDialog.vue @@ -0,0 +1,240 @@ + + + + + + + + Name * + + + + + + + Hostname / IP * + + + + Port + + + + + + + Protocol + + + + + + Group + + + + + + Credential + + + + + + Color (optional) + + + {{ form.color || 'None' }} + Clear + + + + + + Tags + + + Add + + + + {{ tag }} + × + + + + + + + Notes + + + + + {{ error }} + + + + + + + + + + diff --git a/frontend/components/connections/HostTree.vue b/frontend/components/connections/HostTree.vue new file mode 100644 index 0000000..bd1c43b --- /dev/null +++ b/frontend/components/connections/HostTree.vue @@ -0,0 +1,97 @@ + + + + + + + + + + {{ isExpanded(group.id) ? '▾' : '▸' }} + {{ group.name }} + + + + + + + + emit('select-host', h)" + @new-host="(gid) => emit('new-host', gid)" + /> + + + + + {{ host.name }} + {{ host.protocol.toUpperCase() }} + + + + + + + No groups yet + + diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue new file mode 100644 index 0000000..191e82e --- /dev/null +++ b/frontend/layouts/default.vue @@ -0,0 +1,28 @@ + + + + + + + + WRAITH + + + Vault + Settings + Logout + + + + + + + + diff --git a/frontend/pages/index.vue b/frontend/pages/index.vue new file mode 100644 index 0000000..a881258 --- /dev/null +++ b/frontend/pages/index.vue @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + No hosts yet. Click "+ Host" to add your first connection. + + + + + + + + diff --git a/frontend/pages/login.vue b/frontend/pages/login.vue new file mode 100644 index 0000000..f5a0c1e --- /dev/null +++ b/frontend/pages/login.vue @@ -0,0 +1,48 @@ + + + + + + WRAITH + Remote Access Terminal + + + + Email + + + + Password + + + {{ error }} + + {{ loading ? 'Signing in...' : 'Sign In' }} + + + + diff --git a/frontend/stores/auth.store.ts b/frontend/stores/auth.store.ts new file mode 100644 index 0000000..4ad0940 --- /dev/null +++ b/frontend/stores/auth.store.ts @@ -0,0 +1,44 @@ +import { defineStore } from 'pinia' + +interface User { + id: number + email: string + displayName: string | null +} + +export const useAuthStore = defineStore('auth', { + state: () => ({ + token: localStorage.getItem('wraith_token') || '', + user: null as User | null, + }), + getters: { + isAuthenticated: (state) => !!state.token, + }, + actions: { + async login(email: string, password: string) { + const res = await $fetch<{ access_token: string; user: User }>('/api/auth/login', { + method: 'POST', + body: { email, password }, + }) + this.token = res.access_token + this.user = res.user + localStorage.setItem('wraith_token', res.access_token) + }, + logout() { + this.token = '' + this.user = null + localStorage.removeItem('wraith_token') + navigateTo('/login') + }, + async fetchProfile() { + if (!this.token) return + try { + this.user = await $fetch('/api/auth/profile', { + headers: { Authorization: `Bearer ${this.token}` }, + }) + } catch { + this.logout() + } + }, + }, +}) diff --git a/frontend/stores/connection.store.ts b/frontend/stores/connection.store.ts new file mode 100644 index 0000000..4e78247 --- /dev/null +++ b/frontend/stores/connection.store.ts @@ -0,0 +1,98 @@ +import { defineStore } from 'pinia' +import { useAuthStore } from './auth.store' + +interface Host { + id: number + name: string + hostname: string + port: number + protocol: 'ssh' | 'rdp' + groupId: number | null + credentialId: number | null + tags: string[] + notes: string | null + color: string | null + lastConnectedAt: string | null + group: { id: number; name: string } | null +} + +interface HostGroup { + id: number + name: string + parentId: number | null + children: HostGroup[] + hosts: Host[] +} + +export const useConnectionStore = defineStore('connections', { + state: () => ({ + hosts: [] as Host[], + groups: [] as HostGroup[], + search: '', + loading: false, + }), + actions: { + headers() { + const auth = useAuthStore() + return { Authorization: `Bearer ${auth.token}` } + }, + async fetchHosts() { + this.loading = true + try { + this.hosts = await $fetch('/api/hosts', { headers: this.headers() }) + } finally { + this.loading = false + } + }, + async fetchTree() { + this.groups = await $fetch('/api/groups/tree', { headers: this.headers() }) + }, + async createHost(data: Partial) { + const host = await $fetch('/api/hosts', { + method: 'POST', + body: data, + headers: this.headers(), + }) + await this.fetchHosts() + return host + }, + async updateHost(id: number, data: Partial) { + await $fetch(`/api/hosts/${id}`, { + method: 'PUT', + body: data, + headers: this.headers(), + }) + await this.fetchHosts() + }, + async deleteHost(id: number) { + await $fetch(`/api/hosts/${id}`, { + method: 'DELETE', + headers: this.headers(), + }) + await this.fetchHosts() + }, + async createGroup(data: { name: string; parentId?: number }) { + await $fetch('/api/groups', { + method: 'POST', + body: data, + headers: this.headers(), + }) + await this.fetchTree() + }, + async updateGroup(id: number, data: { name?: string; parentId?: number }) { + await $fetch(`/api/groups/${id}`, { + method: 'PUT', + body: data, + headers: this.headers(), + }) + await this.fetchTree() + }, + async deleteGroup(id: number) { + await $fetch(`/api/groups/${id}`, { + method: 'DELETE', + headers: this.headers(), + }) + await this.fetchTree() + }, + }, +})
{{ error }}
{{ host.hostname }}:{{ host.port }}
+ No hosts yet. Click "+ Host" to add your first connection. +
Remote Access Terminal