mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
289 lines
9.5 KiB
TypeScript
289 lines
9.5 KiB
TypeScript
import { Env, Cipher, CipherResponse, Attachment } from '../types';
|
|
import { StorageService } from '../services/storage';
|
|
import { jsonResponse, errorResponse } from '../utils/response';
|
|
import { generateUUID } from '../utils/uuid';
|
|
import { deleteAllAttachmentsForCipher } from './attachments';
|
|
|
|
// Format attachments for API response
|
|
function formatAttachments(attachments: Attachment[]): any[] | null {
|
|
if (attachments.length === 0) return null;
|
|
return attachments.map(a => ({
|
|
id: a.id,
|
|
fileName: a.fileName,
|
|
size: Number(a.size) || 0, // Android expects Int, not String
|
|
sizeName: a.sizeName,
|
|
key: a.key,
|
|
url: `/api/ciphers/${a.cipherId}/attachment/${a.id}`, // Android requires non-null url!
|
|
object: 'attachment',
|
|
}));
|
|
}
|
|
|
|
// Convert internal cipher to API response format
|
|
function cipherToResponse(cipher: Cipher, attachments: Attachment[] = []): CipherResponse {
|
|
return {
|
|
id: cipher.id,
|
|
organizationId: null,
|
|
folderId: cipher.folderId,
|
|
type: Number(cipher.type) || 1,
|
|
name: cipher.name,
|
|
notes: cipher.notes,
|
|
favorite: cipher.favorite,
|
|
login: cipher.login,
|
|
card: cipher.card,
|
|
identity: cipher.identity,
|
|
secureNote: cipher.secureNote,
|
|
sshKey: cipher.sshKey,
|
|
fields: cipher.fields,
|
|
passwordHistory: cipher.passwordHistory,
|
|
reprompt: cipher.reprompt,
|
|
organizationUseTotp: false,
|
|
creationDate: cipher.createdAt,
|
|
revisionDate: cipher.updatedAt,
|
|
deletedDate: cipher.deletedAt,
|
|
archivedDate: null,
|
|
edit: true,
|
|
viewPassword: true,
|
|
permissions: {
|
|
delete: true,
|
|
restore: true,
|
|
},
|
|
object: 'cipher',
|
|
collectionIds: [],
|
|
attachments: formatAttachments(attachments),
|
|
key: cipher.key,
|
|
encryptedFor: null,
|
|
};
|
|
}
|
|
|
|
// GET /api/ciphers
|
|
export async function handleGetCiphers(request: Request, env: Env, userId: string): Promise<Response> {
|
|
const storage = new StorageService(env.DB);
|
|
const ciphers = await storage.getAllCiphers(userId);
|
|
|
|
// Filter out soft-deleted ciphers unless specifically requested
|
|
const url = new URL(request.url);
|
|
const includeDeleted = url.searchParams.get('deleted') === 'true';
|
|
|
|
const filteredCiphers = includeDeleted
|
|
? ciphers
|
|
: ciphers.filter(c => !c.deletedAt);
|
|
|
|
// Get attachments for all ciphers
|
|
const cipherResponses = [];
|
|
for (const cipher of filteredCiphers) {
|
|
const attachments = await storage.getAttachmentsByCipher(cipher.id);
|
|
cipherResponses.push(cipherToResponse(cipher, attachments));
|
|
}
|
|
|
|
return jsonResponse({
|
|
data: cipherResponses,
|
|
object: 'list',
|
|
continuationToken: null,
|
|
});
|
|
}
|
|
|
|
// GET /api/ciphers/:id
|
|
export async function handleGetCipher(request: Request, env: Env, userId: string, id: string): Promise<Response> {
|
|
const storage = new StorageService(env.DB);
|
|
const cipher = await storage.getCipher(id);
|
|
|
|
if (!cipher || cipher.userId !== userId) {
|
|
return errorResponse('Cipher not found', 404);
|
|
}
|
|
|
|
const attachments = await storage.getAttachmentsByCipher(cipher.id);
|
|
return jsonResponse(cipherToResponse(cipher, attachments));
|
|
}
|
|
|
|
// POST /api/ciphers
|
|
export async function handleCreateCipher(request: Request, env: Env, userId: string): Promise<Response> {
|
|
const storage = new StorageService(env.DB);
|
|
|
|
let body: any;
|
|
try {
|
|
body = await request.json();
|
|
} catch {
|
|
return errorResponse('Invalid JSON', 400);
|
|
}
|
|
|
|
// Handle nested cipher object (from some clients)
|
|
// Android client sends PascalCase "Cipher" for organization ciphers
|
|
const cipherData = body.Cipher || body.cipher || body;
|
|
|
|
const now = new Date().toISOString();
|
|
const cipher: Cipher = {
|
|
id: generateUUID(),
|
|
userId: userId,
|
|
type: Number(cipherData.type) || 1,
|
|
folderId: cipherData.folderId || null,
|
|
name: cipherData.name || null,
|
|
notes: cipherData.notes || null,
|
|
favorite: cipherData.favorite || false,
|
|
login: cipherData.login || null,
|
|
card: cipherData.card || null,
|
|
identity: cipherData.identity || null,
|
|
secureNote: cipherData.secureNote || null,
|
|
sshKey: cipherData.sshKey || null,
|
|
fields: cipherData.fields || null,
|
|
passwordHistory: cipherData.passwordHistory || null,
|
|
reprompt: cipherData.reprompt || 0,
|
|
key: cipherData.key || null,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
deletedAt: null,
|
|
};
|
|
|
|
await storage.saveCipher(cipher);
|
|
await storage.updateRevisionDate(userId);
|
|
|
|
return jsonResponse(cipherToResponse(cipher), 200);
|
|
}
|
|
|
|
// PUT /api/ciphers/:id
|
|
export async function handleUpdateCipher(request: Request, env: Env, userId: string, id: string): Promise<Response> {
|
|
const storage = new StorageService(env.DB);
|
|
const existingCipher = await storage.getCipher(id);
|
|
|
|
if (!existingCipher || existingCipher.userId !== userId) {
|
|
return errorResponse('Cipher not found', 404);
|
|
}
|
|
|
|
let body: any;
|
|
try {
|
|
body = await request.json();
|
|
} catch {
|
|
return errorResponse('Invalid JSON', 400);
|
|
}
|
|
|
|
// Handle nested cipher object
|
|
// Android client sends PascalCase "Cipher" for organization ciphers
|
|
const cipherData = body.Cipher || body.cipher || body;
|
|
|
|
const cipher: Cipher = {
|
|
...existingCipher,
|
|
type: Number(cipherData.type) || existingCipher.type,
|
|
folderId: cipherData.folderId !== undefined ? cipherData.folderId : existingCipher.folderId,
|
|
name: cipherData.name ?? existingCipher.name,
|
|
notes: cipherData.notes !== undefined ? cipherData.notes : existingCipher.notes,
|
|
favorite: cipherData.favorite ?? existingCipher.favorite,
|
|
login: cipherData.login !== undefined ? cipherData.login : existingCipher.login,
|
|
card: cipherData.card !== undefined ? cipherData.card : existingCipher.card,
|
|
identity: cipherData.identity !== undefined ? cipherData.identity : existingCipher.identity,
|
|
secureNote: cipherData.secureNote !== undefined ? cipherData.secureNote : existingCipher.secureNote,
|
|
sshKey: cipherData.sshKey !== undefined ? cipherData.sshKey : existingCipher.sshKey,
|
|
fields: cipherData.fields !== undefined ? cipherData.fields : existingCipher.fields,
|
|
passwordHistory: cipherData.passwordHistory !== undefined ? cipherData.passwordHistory : existingCipher.passwordHistory,
|
|
reprompt: cipherData.reprompt ?? existingCipher.reprompt,
|
|
key: cipherData.key !== undefined ? cipherData.key : existingCipher.key,
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
|
|
await storage.saveCipher(cipher);
|
|
await storage.updateRevisionDate(userId);
|
|
|
|
return jsonResponse(cipherToResponse(cipher));
|
|
}
|
|
|
|
// DELETE /api/ciphers/:id
|
|
export async function handleDeleteCipher(request: Request, env: Env, userId: string, id: string): Promise<Response> {
|
|
const storage = new StorageService(env.DB);
|
|
const cipher = await storage.getCipher(id);
|
|
|
|
if (!cipher || cipher.userId !== userId) {
|
|
return errorResponse('Cipher not found', 404);
|
|
}
|
|
|
|
// Soft delete
|
|
cipher.deletedAt = new Date().toISOString();
|
|
cipher.updatedAt = cipher.deletedAt;
|
|
await storage.saveCipher(cipher);
|
|
await storage.updateRevisionDate(userId);
|
|
|
|
return jsonResponse(cipherToResponse(cipher));
|
|
}
|
|
|
|
// DELETE /api/ciphers/:id (permanent)
|
|
export async function handlePermanentDeleteCipher(request: Request, env: Env, userId: string, id: string): Promise<Response> {
|
|
const storage = new StorageService(env.DB);
|
|
const cipher = await storage.getCipher(id);
|
|
|
|
if (!cipher || cipher.userId !== userId) {
|
|
return errorResponse('Cipher not found', 404);
|
|
}
|
|
|
|
// Delete all attachments first
|
|
await deleteAllAttachmentsForCipher(env, id);
|
|
|
|
await storage.deleteCipher(id, userId);
|
|
await storage.updateRevisionDate(userId);
|
|
|
|
return new Response(null, { status: 204 });
|
|
}
|
|
|
|
// PUT /api/ciphers/:id/restore
|
|
export async function handleRestoreCipher(request: Request, env: Env, userId: string, id: string): Promise<Response> {
|
|
const storage = new StorageService(env.DB);
|
|
const cipher = await storage.getCipher(id);
|
|
|
|
if (!cipher || cipher.userId !== userId) {
|
|
return errorResponse('Cipher not found', 404);
|
|
}
|
|
|
|
cipher.deletedAt = null;
|
|
cipher.updatedAt = new Date().toISOString();
|
|
await storage.saveCipher(cipher);
|
|
await storage.updateRevisionDate(userId);
|
|
|
|
return jsonResponse(cipherToResponse(cipher));
|
|
}
|
|
|
|
// PUT /api/ciphers/:id/partial - Update only favorite/folderId
|
|
export async function handlePartialUpdateCipher(request: Request, env: Env, userId: string, id: string): Promise<Response> {
|
|
const storage = new StorageService(env.DB);
|
|
const cipher = await storage.getCipher(id);
|
|
|
|
if (!cipher || cipher.userId !== userId) {
|
|
return errorResponse('Cipher not found', 404);
|
|
}
|
|
|
|
let body: { folderId?: string | null; favorite?: boolean };
|
|
try {
|
|
body = await request.json();
|
|
} catch {
|
|
return errorResponse('Invalid JSON', 400);
|
|
}
|
|
|
|
if (body.folderId !== undefined) {
|
|
cipher.folderId = body.folderId;
|
|
}
|
|
if (body.favorite !== undefined) {
|
|
cipher.favorite = body.favorite;
|
|
}
|
|
cipher.updatedAt = new Date().toISOString();
|
|
|
|
await storage.saveCipher(cipher);
|
|
await storage.updateRevisionDate(userId);
|
|
|
|
return jsonResponse(cipherToResponse(cipher));
|
|
}
|
|
|
|
// POST/PUT /api/ciphers/move - Bulk move to folder
|
|
export async function handleBulkMoveCiphers(request: Request, env: Env, userId: string): Promise<Response> {
|
|
const storage = new StorageService(env.DB);
|
|
|
|
let body: { ids?: string[]; folderId?: string | null };
|
|
try {
|
|
body = await request.json();
|
|
} catch {
|
|
return errorResponse('Invalid JSON', 400);
|
|
}
|
|
|
|
if (!body.ids || !Array.isArray(body.ids)) {
|
|
return errorResponse('ids array is required', 400);
|
|
}
|
|
|
|
await storage.bulkMoveCiphers(body.ids, body.folderId || null, userId);
|
|
|
|
return new Response(null, { status: 204 });
|
|
}
|