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.
- package/README.md +157 -0
- package/package.json +69 -0
- package/src/VaultApp.tsx +82 -0
- package/src/components/datetime-picker.tsx +262 -0
- package/src/components/dialogs.tsx +467 -0
- package/src/components/searchable-multi-select.tsx +275 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/calendar.tsx +210 -0
- package/src/components/ui/command.tsx +181 -0
- package/src/components/ui/dialog.tsx +140 -0
- package/src/components/ui/input.tsx +27 -0
- package/src/components/ui/label.tsx +21 -0
- package/src/components/ui/popover.tsx +45 -0
- package/src/components/ui/scroll-area.tsx +64 -0
- package/src/components/ui/separator.tsx +25 -0
- package/src/components/ui/table.tsx +122 -0
- package/src/components/vault/entries-table.tsx +283 -0
- package/src/components/vault/password-cell.tsx +26 -0
- package/src/context/VaultProvider.tsx +49 -0
- package/src/hooks/useVault.ts +177 -0
- package/src/index.tsx +99 -0
- package/src/lib/utils.ts +6 -0
- package/src/pages/AuditPage.tsx +408 -0
- package/src/pages/EntriesPage.tsx +111 -0
- package/src/pages/VaultPage.tsx +184 -0
- package/src/pages/meyi-connect-password.code-workspace +11 -0
- package/src/services/api.ts +139 -0
- package/src/style.css +5 -0
|
@@ -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,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
|
+
}
|