137 lines
4.1 KiB
JavaScript
137 lines
4.1 KiB
JavaScript
/**
|
|
* Data model definitions and factory functions.
|
|
*
|
|
* All IDs are generated with a simple ULID-like timestamp + random suffix
|
|
* to keep things sortable and collision-resistant without external deps.
|
|
*/
|
|
|
|
/**
|
|
* Generate a unique ID (timestamp-based, sortable).
|
|
* @returns {string}
|
|
*/
|
|
export function generateId() {
|
|
const timestamp = Date.now().toString(36)
|
|
const random = Math.random().toString(36).slice(2, 10)
|
|
return `${timestamp}_${random}`
|
|
}
|
|
|
|
/**
|
|
* @typedef {Object} CredentialEntry
|
|
* @property {string} id - Unique identifier
|
|
* @property {string} title - Display name (e.g. "GitHub", "Gmail")
|
|
* @property {string} username - Login username or email
|
|
* @property {string} encryptedPassword - AES-GCM encrypted password blob (JSON string)
|
|
* @property {string} [url] - Website URL
|
|
* @property {string} [notes] - Free-form notes
|
|
* @property {string} [groupId] - Reference to a Group id (empty string = no group)
|
|
* @property {string[]} [tags] - Free-form tags
|
|
* @property {string} createdAt - ISO timestamp
|
|
* @property {string} updatedAt - ISO timestamp
|
|
*/
|
|
|
|
/**
|
|
* Create a new CredentialEntry.
|
|
*
|
|
* @param {Object} data
|
|
* @param {string} data.title
|
|
* @param {string} data.username
|
|
* @param {string} data.encryptedPassword - Must already be encrypted
|
|
* @param {string} [data.url]
|
|
* @param {string} [data.notes]
|
|
* @param {string} [data.groupId]
|
|
* @param {string[]} [data.tags]
|
|
* @returns {CredentialEntry}
|
|
*/
|
|
export function createEntry(data) {
|
|
const now = new Date().toISOString()
|
|
return {
|
|
id: generateId(),
|
|
title: data.title.trim(),
|
|
username: data.username.trim(),
|
|
encryptedPassword: data.encryptedPassword,
|
|
url: data.url?.trim() || '',
|
|
notes: data.notes?.trim() || '',
|
|
groupId: data.groupId || '',
|
|
tags: data.tags || [],
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update an existing entry, preserving id and createdAt.
|
|
*
|
|
* @param {CredentialEntry} existing
|
|
* @param {Object} data - Fields to update
|
|
* @returns {CredentialEntry}
|
|
*/
|
|
export function updateEntry(existing, data) {
|
|
return {
|
|
...existing,
|
|
title: data.title !== undefined ? data.title.trim() : existing.title,
|
|
username: data.username !== undefined ? data.username.trim() : existing.username,
|
|
encryptedPassword: data.encryptedPassword !== undefined ? data.encryptedPassword : existing.encryptedPassword,
|
|
url: data.url !== undefined ? data.url.trim() : existing.url,
|
|
notes: data.notes !== undefined ? data.notes.trim() : existing.notes,
|
|
groupId: data.groupId !== undefined ? data.groupId : existing.groupId,
|
|
tags: data.tags !== undefined ? data.tags : existing.tags,
|
|
updatedAt: new Date().toISOString(),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @typedef {Object} Group
|
|
* @property {string} id - Unique identifier
|
|
* @property {string} name - Display name
|
|
* @property {string} [color] - Hex color for UI accent
|
|
* @property {string} createdAt - ISO timestamp
|
|
*/
|
|
|
|
const GROUP_COLORS = [
|
|
'#6c63ff', '#e5484d', '#34d399', '#fbbf24', '#3b82f6',
|
|
'#ec4899', '#8b5cf6', '#14b8a6', '#f97316', '#06b6d4',
|
|
]
|
|
|
|
/**
|
|
* Create a new Group.
|
|
*
|
|
* @param {string} name
|
|
* @param {string} [color]
|
|
* @returns {Group}
|
|
*/
|
|
export function createGroup(name, color) {
|
|
return {
|
|
id: generateId(),
|
|
name: name.trim(),
|
|
color: color || GROUP_COLORS[Math.floor(Math.random() * GROUP_COLORS.length)],
|
|
createdAt: new Date().toISOString(),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate a credential entry form submission.
|
|
*
|
|
* @param {Object} data
|
|
* @returns {{ valid: boolean, errors: string[] }}
|
|
*/
|
|
export function validateEntry(data) {
|
|
const errors = []
|
|
if (!data.title || !data.title.trim()) errors.push('Title is required')
|
|
if (!data.username || !data.username.trim()) errors.push('Username is required')
|
|
if (!data.encryptedPassword) errors.push('Password is required')
|
|
return { valid: errors.length === 0, errors }
|
|
}
|
|
|
|
/**
|
|
* Validate a group name.
|
|
*
|
|
* @param {string} name
|
|
* @returns {{ valid: boolean, errors: string[] }}
|
|
*/
|
|
export function validateGroup(name) {
|
|
const errors = []
|
|
if (!name || !name.trim()) errors.push('Group name is required')
|
|
else if (name.trim().length > 50) errors.push('Group name must be 50 characters or less')
|
|
return { valid: errors.length === 0, errors }
|
|
}
|