meyi-vault-client-dev 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,184 @@
1
+ import { useState } from 'react'
2
+ import { FolderLock, MoreHorizontal, Plus, Shield, Trash2, Pencil, ScrollText, ChevronRight } from 'lucide-react'
3
+ import { useVaults, useDeleteVault, useMe } from '@/hooks/useVault'
4
+ import { VaultCreateDialog, GrantAccessDialog, ConfirmDialog } from '@/components/dialogs'
5
+ import type { Vault } from '@/services/api'
6
+
7
+ interface VaultPageProps {
8
+ onNavigate: (to: 'entries' | 'audit', vaultId?: string) => void
9
+ }
10
+
11
+ export function VaultPage({ onNavigate }: VaultPageProps) {
12
+ const { data: me } = useMe()
13
+ const { data: vaults = [], isLoading } = useVaults()
14
+ const deleteVault = useDeleteVault()
15
+ const [createOpen, setCreateOpen] = useState(false)
16
+ const [editTarget, setEditTarget] = useState<Vault | null>(null)
17
+ const [deleteTarget, setDeleteTarget] = useState<Vault | null>(null)
18
+ const [grantTarget, setGrantTarget] = useState<Vault | null>(null)
19
+ const [menuOpen, setMenuOpen] = useState<string | null>(null)
20
+
21
+ const handleDelete = async () => {
22
+ if (!deleteTarget) return
23
+ await deleteVault.mutateAsync(deleteTarget.id)
24
+ setDeleteTarget(null)
25
+ }
26
+
27
+ const isAdmin = me?.role === 'admin'
28
+
29
+ return (
30
+ <div className='flex flex-col h-full bg-background'>
31
+ {/* Header */}
32
+ <header className='flex items-center justify-between px-6 py-4 border-b border-border'>
33
+ <div className='flex items-center gap-3'>
34
+ <div className='rounded-xl bg-primary/10 p-2.5'>
35
+ <Shield className='h-6 w-6 text-primary' />
36
+ </div>
37
+ <div>
38
+ <h1 className='text-2xl font-bold tracking-tight text-foreground'>Vaults</h1>
39
+ <p className='text-sm text-muted-foreground'>Securely manage and share your encrypted credentials.</p>
40
+ </div>
41
+ </div>
42
+ <div className='flex items-center gap-2'>
43
+ {isAdmin && (
44
+ <button
45
+ onClick={() => onNavigate('audit')}
46
+ className='inline-flex items-center gap-2 rounded-lg border border-input bg-background px-4 py-2 text-sm font-semibold text-foreground shadow-sm hover:bg-muted transition-all'
47
+ >
48
+ <ScrollText className='h-4 w-4' />
49
+ <span>Audit Log</span>
50
+ </button>
51
+ )}
52
+ <button
53
+ onClick={() => { setEditTarget(null); setCreateOpen(true) }}
54
+ className='inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground shadow-sm hover:bg-primary/90 transition-all hover:scale-[1.02] active:scale-[0.98]'
55
+ >
56
+ <Plus className='h-4 w-4' />
57
+ <span>New Vault</span>
58
+ </button>
59
+ </div>
60
+ </header>
61
+
62
+ {/* Content */}
63
+ <main className='flex-1 overflow-y-auto p-6'>
64
+ {isLoading ? (
65
+ <div className='grid gap-4 sm:grid-cols-2 lg:grid-cols-3'>
66
+ {[1, 2, 3].map((i) => (
67
+ <div key={i} className='h-40 rounded-xl border bg-muted/20 animate-pulse' />
68
+ ))}
69
+ </div>
70
+ ) : vaults.length === 0 ? (
71
+ <div className='flex flex-col items-center justify-center p-6 py-12 text-center max-w-sm mx-auto mt-8'>
72
+ <div className='mb-4 rounded-full bg-muted p-4 border border-primary/10'>
73
+ <FolderLock className='h-8 w-8 text-muted-foreground' />
74
+ </div>
75
+ <h2 className='text-lg font-bold tracking-tight mb-1'>No vaults found</h2>
76
+ <p className='text-muted-foreground mb-6 text-sm leading-relaxed'>
77
+ Create your first vault to start storing encrypted credentials for your projects and teams.
78
+ </p>
79
+ </div>
80
+ ) : (
81
+ <div className='grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'>
82
+ {vaults.map((vault) => (
83
+ <div
84
+ key={vault.id}
85
+ className='group relative rounded-xl border bg-card p-4 cursor-pointer hover:shadow-md hover:border-primary/50 transition-all'
86
+ onClick={() => onNavigate('entries', vault.id)}
87
+ >
88
+ <div className='flex items-start justify-between mb-4'>
89
+ <div className='rounded-lg bg-primary/10 p-2.5 transition-colors group-hover:bg-primary/20'>
90
+ <FolderLock className='h-5 w-5 text-primary' />
91
+ </div>
92
+ {vault.is_owner && (
93
+ <div className='relative'>
94
+ <button
95
+ onClick={(e) => { e.stopPropagation(); setMenuOpen(menuOpen === vault.id ? null : vault.id) }}
96
+ className='h-8 w-8 flex items-center justify-center rounded-md hover:bg-muted opacity-0 group-hover:opacity-100 transition-opacity'
97
+ >
98
+ <MoreHorizontal className='h-4 w-4' />
99
+ </button>
100
+ {menuOpen === vault.id && (
101
+ <>
102
+ <div className='fixed inset-0 z-10' onClick={(e) => { e.stopPropagation(); setMenuOpen(null) }} />
103
+ <div className='absolute right-0 top-9 z-20 w-40 rounded-lg border bg-popover shadow-lg py-1.5 overflow-hidden animate-in fade-in zoom-in-95 duration-100'>
104
+ <button
105
+ onClick={(e) => {
106
+ e.stopPropagation();
107
+ setMenuOpen(null);
108
+ setEditTarget(vault);
109
+ setCreateOpen(true);
110
+ }}
111
+ className='w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted text-left transition-colors'
112
+ >
113
+ <Pencil className='h-4 w-4 text-muted-foreground' />
114
+ <span>Edit</span>
115
+ </button>
116
+ <button
117
+ onClick={(e) => { e.stopPropagation(); setMenuOpen(null); setGrantTarget(vault) }}
118
+ className='w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted text-left transition-colors'
119
+ >
120
+ <Shield className='h-4 w-4 text-muted-foreground' />
121
+ <span>Access</span>
122
+ </button>
123
+ <div className='h-px bg-border my-1' />
124
+ <button
125
+ onClick={(e) => { e.stopPropagation(); setMenuOpen(null); setDeleteTarget(vault) }}
126
+ className='w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted text-left text-destructive transition-colors'
127
+ >
128
+ <Trash2 className='h-4 w-4' />
129
+ <span>Delete</span>
130
+ </button>
131
+ </div>
132
+ </>
133
+ )}
134
+ </div>
135
+ )}
136
+ </div>
137
+
138
+ <div className='space-y-1'>
139
+ <p className='font-bold text-lg text-foreground truncate'>{vault.name}</p>
140
+ <p className='text-xs text-muted-foreground font-medium'>
141
+ {vault.is_owner ? 'Owner' : 'Shared with you'}
142
+ </p>
143
+ </div>
144
+
145
+ <div className='flex items-center justify-between mt-6 pt-4 border-t'>
146
+ <span className='text-[10px] tracking-wider text-muted-foreground'>
147
+ Created {new Date(vault.created_at).toLocaleDateString()}
148
+ </span>
149
+ <div className='rounded-full p-1 group-hover:bg-primary/10 transition-colors'>
150
+ <ChevronRight className='h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors' />
151
+ </div>
152
+ </div>
153
+ </div>
154
+ ))}
155
+ </div>
156
+ )}
157
+ </main>
158
+
159
+ <VaultCreateDialog
160
+ open={createOpen}
161
+ onOpenChange={setCreateOpen}
162
+ editVault={editTarget || undefined}
163
+ />
164
+
165
+ <GrantAccessDialog
166
+ open={!!grantTarget}
167
+ onOpenChange={(o) => { if (!o) setGrantTarget(null) }}
168
+ scope='vault'
169
+ scopeId={grantTarget?.id || ''}
170
+ scopeLabel={grantTarget?.name || ''}
171
+ />
172
+
173
+ <ConfirmDialog
174
+ open={!!deleteTarget}
175
+ onOpenChange={(o) => { if (!o) setDeleteTarget(null) }}
176
+ title='Delete Vault'
177
+ description={`Are you sure you want to delete "${deleteTarget?.name}"? This will permanently delete all credentials within this vault.`}
178
+ onConfirm={handleDelete}
179
+ loading={deleteVault.isPending}
180
+ />
181
+ </div>
182
+ )
183
+ }
184
+
@@ -0,0 +1,11 @@
1
+ {
2
+ "folders": [
3
+ {
4
+ "path": "../../../../meyi-connect-password"
5
+ },
6
+ {
7
+ "path": "../../.."
8
+ }
9
+ ],
10
+ "settings": {}
11
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * API client for vault-client.
3
+ *
4
+ * The host (MeyiConnect) injects its configured axios instance via
5
+ * VaultProvider so we share the same base URL, auth interceptors,
6
+ * and token refresh logic — no duplicate auth code.
7
+ */
8
+
9
+ import type { AxiosInstance } from 'axios'
10
+
11
+ let _api: AxiosInstance | null = null
12
+
13
+ export function setApiInstance(instance: AxiosInstance) {
14
+ _api = instance
15
+ }
16
+
17
+ function getApi(): AxiosInstance {
18
+ if (!_api) throw new Error('[vault-client] No API instance. Wrap your app in <VaultProvider api={...} />')
19
+ return _api
20
+ }
21
+
22
+ // ── Types ─────────────────────────────────────────────────────────────────────
23
+
24
+ export interface Vault {
25
+ id: string
26
+ name: string
27
+ owner_id: string
28
+ created_at: string
29
+ is_owner: boolean
30
+ }
31
+
32
+ export interface VaultEntry {
33
+ id: string
34
+ vault_id: string
35
+ domain: string
36
+ alias: string
37
+ username: string
38
+ password?: string | null
39
+ notes: string
40
+ created_at: string
41
+ updated_at: string
42
+ }
43
+
44
+ export interface Grant {
45
+ id: string
46
+ scope: 'vault' | 'entry'
47
+ scope_id: string
48
+ grantee_id: string
49
+ granted_by_id: string
50
+ expires_at: string | null
51
+ revoked_at: string | null
52
+ created_at: string
53
+ }
54
+
55
+ export interface AuditLog {
56
+ id: string
57
+ actor_id: string
58
+ actor_name: string
59
+ actor_email: string
60
+ action: string
61
+ module: string
62
+ item_id: string
63
+ item_label: string
64
+ meta: any
65
+ created_at: string
66
+ }
67
+
68
+ export interface AuditResponse {
69
+ logs: AuditLog[]
70
+ total: number
71
+ page: number
72
+ limit: number
73
+ total_pages: number
74
+ }
75
+
76
+ export type ExpiresIn = '1h' | '8h' | '24h' | '7d' | '30d' | null
77
+
78
+ // ── API functions ─────────────────────────────────────────────────────────────
79
+
80
+ export const vaultApi = {
81
+ // Vaults
82
+ listVaults: () =>
83
+ getApi().get<{ vaults: Vault[] }>('/vault/vaults').then((r) => r.data.vaults),
84
+
85
+ createVault: (name: string) =>
86
+ getApi().post<{ vault: Vault }>('/vault/vaults', { name }).then((r) => r.data.vault),
87
+
88
+ updateVault: (id: string, name: string) =>
89
+ getApi().put<{ vault: Vault }>(`/vault/vaults/${id}`, { name }).then((r) => r.data.vault),
90
+
91
+ deleteVault: (id: string) =>
92
+ getApi().delete(`/vault/vaults/${id}`),
93
+
94
+ // Entries
95
+ listEntries: (vaultId: string) =>
96
+ getApi()
97
+ .get<{ entries: VaultEntry[] }>(`/vault/vaults/${vaultId}/entries`)
98
+ .then((r) => r.data.entries),
99
+
100
+ createEntry: (vaultId: string, data: { alias?: string; username?: string; password?: string; domain?: string; notes?: string }) =>
101
+ getApi()
102
+ .post<{ entry: VaultEntry }>(`/vault/vaults/${vaultId}/entries`, data)
103
+ .then((r) => r.data.entry),
104
+
105
+ updateEntry: (vaultId: string, entryId: string, data: Partial<VaultEntry>) =>
106
+ getApi()
107
+ .put<{ entry: VaultEntry }>(`/vault/vaults/${vaultId}/entries/${entryId}`, data)
108
+ .then((r) => r.data.entry),
109
+
110
+ deleteEntry: (vaultId: string, entryId: string) =>
111
+ getApi().delete(`/vault/vaults/${vaultId}/entries/${entryId}`),
112
+
113
+ // Grants
114
+ createGrant: (data: { scope: 'vault' | 'entry'; scope_id: string; grantee_id: string; expires_in: ExpiresIn }) =>
115
+ getApi().post<{ grant: Grant }>('/vault/grants', data).then((r) => r.data.grant),
116
+
117
+ listGrants: () =>
118
+ getApi().get<{ grants: Grant[] }>('/vault/grants').then((r) => r.data.grants),
119
+
120
+ revokeGrant: (id: string) =>
121
+ getApi().delete(`/vault/grants/${id}`),
122
+
123
+ // Stats & User
124
+ stats: () =>
125
+ getApi()
126
+ .get<{ vault_count: number; active_grants: number; entry_count: number }>('/vault/stats')
127
+ .then((r) => r.data),
128
+
129
+ getMe: () =>
130
+ getApi()
131
+ .get<{ user: { id: string; full_name: string; email: string; role: string } }>('/vault/stats/me')
132
+ .then((r) => r.data.user),
133
+
134
+ // Audit Logs
135
+ listAuditLogs: (params: { page?: number; limit?: number; search?: string; module?: string }) =>
136
+ getApi()
137
+ .get<AuditResponse>('/vault/audit', { params })
138
+ .then((r) => r.data),
139
+ }
package/src/style.css ADDED
@@ -0,0 +1,5 @@
1
+ /* vault-client styles
2
+ Styles are provided by the host (MeyiConnect) via Tailwind CSS.
3
+ This file is intentionally minimal — it exists so vault-client/style.css
4
+ resolves correctly when imported by the host application.
5
+ */