2026-05-11 22:32:05 +00:00

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 }
}