mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 02:01:10 +02:00
feat(crypto): phase 7.2 — encrypt storeless modules (questions, links, documents, meals)
Five storeless modules whose writes happen directly from view files
(no central store yet) get the same encryption treatment by wrapping
each .add/.update call site with encryptRecord and each read site
with decryptRecord(s). Registry entries are also corrected to match
the actual schemas — the previous Phase 1 placeholder names guessed
the wrong field names.
Registry corrections + flips
----------------------------
- meals: was ['description', 'notes', 'aiAnalysis'] → now
['description', 'portionSize'] (LocalMeal has neither notes nor
aiAnalysis on the schema; portionSize is a short user label same
sensitivity as description)
- documents: was ['title', 'content', 'body'] → now
['title', 'content'] (LocalDocument uses content, no body column)
- links: was ['title', 'description', 'targetUrl'] → now
['title', 'description']. originalUrl STAYS PLAINTEXT — the
public redirect handler resolves shortCode → originalUrl on every
click, encrypting it would force the redirect path to do an async
decrypt before issuing the 302
- questions: was ['title', 'body', 'notes'] → now
['title', 'description'] (LocalQuestion uses description)
- answers: was ['body'] → now ['content'] (LocalAnswer uses content)
All five tables flipped to enabled:true.
Write sites wrapped
-------------------
Each call site builds the row/diff as a typed object, runs
encryptRecord on it, then calls table.add / table.update:
- questions/views/DetailView.svelte (saveField)
- questions/[id]/+page.svelte (saveEdit + answer.add)
- questions/new/+page.svelte (initial create)
- uload/+page.svelte (createLink + saveEdit)
- uload/views/DetailView.svelte (saveField)
- context/documents/+page.svelte (handleCreateDocument)
- context/documents/[id]/+page.svelte (handleSave with encrypted diff)
- context/spaces/[id]/+page.svelte (handleCreateDocument)
- nutriphi/add/+page.svelte (handleSubmit)
Pure metadata writes (toggle pinned, toggle isActive, soft-delete via
deletedAt) are intentionally NOT wrapped — they touch zero encrypted
fields so encryptRecord would be a no-op anyway.
Read sites decrypted
--------------------
- questions/queries.ts: useAllQuestions, useAnswersByQuestion
- questions/views/DetailView.svelte (liveQuery clone)
- questions/ListView.svelte (Workbench)
- uload/queries.ts: allLinks$, useAllLinks, useLinkById
- uload/views/DetailView.svelte (liveQuery clone)
- uload/ListView.svelte
- uload/settings/+page.svelte (decrypts before serializing the
JSON export — otherwise the user would download ciphertext)
- context/queries.ts: useAllDocuments, useSpaceDocuments
- context/ListView.svelte
- cross-app-queries.useRecentDocuments (dashboard widget)
- nutriphi/queries.ts: useAllMeals
- nutriphi/ListView.svelte
The cards/dashboard widget for nutrition only reads m.nutrition (the
plaintext numeric breakdown), so it stays untouched. nutriphi/history
benefits transparently because it consumes useAllMeals which now
decrypts.
Why
---
Closes the second-tier plaintext gaps. The five tables flipped here
were on the registry from day one but stuck behind enabled:false
because no central store existed to hook into. Phase 7.2 takes the
pragmatic approach of wrapping at each call site rather than blocking
on a store extraction refactor — same end result for security, much
smaller diff. A future store consolidation pass can collapse the
duplication without changing the encryption surface.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c875b4e966
commit
40b7069eb0
20 changed files with 152 additions and 73 deletions
|
|
@ -272,13 +272,17 @@ export function useRecentDecks(limit = 5) {
|
|||
/** Recent documents + spaces. */
|
||||
export function useRecentDocuments(limit = 5) {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
return db
|
||||
// title + content are encrypted on disk; the dashboard surfaces the
|
||||
// title so we have to decrypt before returning. limit is applied
|
||||
// pre-decrypt to keep the batch small.
|
||||
const visible = await db
|
||||
.table<LocalDocument>('documents')
|
||||
.orderBy('updatedAt')
|
||||
.reverse()
|
||||
.filter((d) => !d.deletedAt)
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
return decryptRecords('documents', visible);
|
||||
}, [] as LocalDocument[]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -117,7 +117,12 @@ export const ENCRYPTION_REGISTRY: Record<string, EncryptionConfig> = {
|
|||
cycleDayLogs: { enabled: true, fields: ['notes', 'mood'] },
|
||||
|
||||
// ─── NutriPhi ────────────────────────────────────────────
|
||||
meals: { enabled: false, fields: ['description', 'notes', 'aiAnalysis'] },
|
||||
// LocalMeal only has `description` as user-typed text (mealType /
|
||||
// inputType / nutrition numbers stay plaintext for the daily-summary
|
||||
// aggregations and the calorie-progress widget). portionSize is a
|
||||
// short label like "1 Tasse" — same sensitivity as description, so
|
||||
// we encrypt it too.
|
||||
meals: { enabled: true, fields: ['description', 'portionSize'] },
|
||||
|
||||
// ─── Planta ──────────────────────────────────────────────
|
||||
// `name` is NOT in the schema index for plants (only isActive +
|
||||
|
|
@ -140,7 +145,11 @@ export const ENCRYPTION_REGISTRY: Record<string, EncryptionConfig> = {
|
|||
slides: { enabled: true, fields: ['content'] },
|
||||
|
||||
// ─── Context ─────────────────────────────────────────────
|
||||
documents: { enabled: false, fields: ['title', 'content', 'body'] },
|
||||
// LocalDocument has `title` + `content` (no `body` column on the
|
||||
// schema). DocumentType (text/context/prompt) and the spaceId
|
||||
// foreign key stay plaintext so the workspace tree still groups
|
||||
// documents per space without a key.
|
||||
documents: { enabled: true, fields: ['title', 'content'] },
|
||||
|
||||
// ─── Storage ─────────────────────────────────────────────
|
||||
files: { enabled: false, fields: ['name', 'originalName', 'notes'] },
|
||||
|
|
@ -153,12 +162,12 @@ export const ENCRYPTION_REGISTRY: Record<string, EncryptionConfig> = {
|
|||
mukkePlaylists: { enabled: false, fields: ['name', 'description'] },
|
||||
|
||||
// ─── Questions ───────────────────────────────────────────
|
||||
// Writes from views are not yet routed through a store — registry
|
||||
// is set so future store creation gets encryption automatically;
|
||||
// existing direct db.table().update() call sites in the views need
|
||||
// to migrate to a store before they actually flow through encryptRecord.
|
||||
questions: { enabled: false, fields: ['title', 'body', 'notes'] },
|
||||
answers: { enabled: false, fields: ['body'] },
|
||||
// LocalQuestion uses `title` + `description`; LocalAnswer uses
|
||||
// `content` (not `body`). The view-driven write sites are wrapped
|
||||
// directly via encryptRecord at each call site since this module
|
||||
// has no central store yet.
|
||||
questions: { enabled: true, fields: ['title', 'description'] },
|
||||
answers: { enabled: true, fields: ['content'] },
|
||||
|
||||
// ─── Events (social gatherings) ──────────────────────────
|
||||
socialEvents: { enabled: false, fields: ['title', 'description', 'notes'] },
|
||||
|
|
@ -172,7 +181,13 @@ export const ENCRYPTION_REGISTRY: Record<string, EncryptionConfig> = {
|
|||
transactions: { enabled: true, fields: ['description', 'note'] },
|
||||
|
||||
// ─── uLoad ───────────────────────────────────────────────
|
||||
links: { enabled: false, fields: ['title', 'description', 'targetUrl'] },
|
||||
// `originalUrl` STAYS PLAINTEXT — the redirect handler resolves
|
||||
// shortCode → originalUrl on every click, encrypting it would force
|
||||
// the public redirect path to do an async decrypt before the 302.
|
||||
// shortCode is a public lookup key. We encrypt the user-typed
|
||||
// metadata (title + description) which is the part the user actually
|
||||
// expects to be private, and leave the routing primitives alone.
|
||||
links: { enabled: true, fields: ['title', 'description'] },
|
||||
manaLinks: { enabled: false, fields: ['label', 'url', 'notes'] },
|
||||
|
||||
// ─── Inventar ────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
<script lang="ts">
|
||||
import { liveQuery } from 'dexie';
|
||||
import { db } from '$lib/data/database';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import type { LocalContextSpace, LocalDocument } from './types';
|
||||
|
||||
let spaces = $state<LocalContextSpace[]>([]);
|
||||
|
|
@ -24,10 +25,9 @@
|
|||
|
||||
$effect(() => {
|
||||
const sub = liveQuery(async () => {
|
||||
return db
|
||||
.table<LocalDocument>('documents')
|
||||
.toArray()
|
||||
.then((all) => all.filter((d) => !d.deletedAt));
|
||||
const all = await db.table<LocalDocument>('documents').toArray();
|
||||
const visible = all.filter((d) => !d.deletedAt);
|
||||
return decryptRecords('documents', visible);
|
||||
}).subscribe((val) => {
|
||||
documents = val ?? [];
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
|
||||
import { db } from '$lib/data/database';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import type { LocalContextSpace, LocalDocument, Space, Document, DocumentType } from './types';
|
||||
|
||||
// ─── Type Converters ──────────────────────────────────────
|
||||
|
|
@ -60,8 +61,9 @@ export function useAllSpaces() {
|
|||
export function useAllDocuments() {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalDocument>('documents').toArray();
|
||||
return locals
|
||||
.filter((d) => !d.deletedAt)
|
||||
const visible = locals.filter((d) => !d.deletedAt);
|
||||
const decrypted = await decryptRecords('documents', visible);
|
||||
return decrypted
|
||||
.map(toDocument)
|
||||
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime());
|
||||
}, [] as Document[]);
|
||||
|
|
@ -75,8 +77,9 @@ export function useSpaceDocuments(spaceId: string) {
|
|||
.where('spaceId')
|
||||
.equals(spaceId)
|
||||
.toArray();
|
||||
return locals
|
||||
.filter((d) => !d.deletedAt)
|
||||
const visible = locals.filter((d) => !d.deletedAt);
|
||||
const decrypted = await decryptRecords('documents', visible);
|
||||
return decrypted
|
||||
.map(toDocument)
|
||||
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime());
|
||||
}, [] as Document[]);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
<script lang="ts">
|
||||
import { liveQuery } from 'dexie';
|
||||
import { db } from '$lib/data/database';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import type { LocalMeal, LocalGoal } from './types';
|
||||
|
||||
let meals = $state<LocalMeal[]>([]);
|
||||
|
|
@ -14,10 +15,9 @@
|
|||
|
||||
$effect(() => {
|
||||
const sub = liveQuery(async () => {
|
||||
return db
|
||||
.table<LocalMeal>('meals')
|
||||
.toArray()
|
||||
.then((all) => all.filter((m) => !m.deletedAt));
|
||||
const all = await db.table<LocalMeal>('meals').toArray();
|
||||
const visible = all.filter((m) => !m.deletedAt);
|
||||
return decryptRecords('meals', visible);
|
||||
}).subscribe((val) => {
|
||||
meals = val ?? [];
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { liveQuery } from 'dexie';
|
||||
import { db } from '$lib/data/database';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import type {
|
||||
LocalMeal,
|
||||
LocalGoal,
|
||||
|
|
@ -39,7 +40,9 @@ export function toMealWithNutrition(local: LocalMeal): MealWithNutrition {
|
|||
export function useAllMeals() {
|
||||
return liveQuery(async () => {
|
||||
const locals = await db.table<LocalMeal>('meals').toArray();
|
||||
return locals.filter((m) => !m.deletedAt).map(toMealWithNutrition);
|
||||
const visible = locals.filter((m) => !m.deletedAt);
|
||||
const decrypted = await decryptRecords('meals', visible);
|
||||
return decrypted.map(toMealWithNutrition);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
<script lang="ts">
|
||||
import { liveQuery } from 'dexie';
|
||||
import { db } from '$lib/data/database';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import type { LocalQuestion, LocalCollection } from './types';
|
||||
import type { ViewProps } from '$lib/app-registry';
|
||||
|
||||
|
|
@ -15,10 +16,9 @@
|
|||
|
||||
$effect(() => {
|
||||
const sub = liveQuery(async () => {
|
||||
return db
|
||||
.table<LocalQuestion>('questions')
|
||||
.toArray()
|
||||
.then((all) => all.filter((q) => !q.deletedAt));
|
||||
const all = await db.table<LocalQuestion>('questions').toArray();
|
||||
const visible = all.filter((q) => !q.deletedAt);
|
||||
return decryptRecords('questions', visible);
|
||||
}).subscribe((val) => {
|
||||
questions = val ?? [];
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { liveQuery } from 'dexie';
|
||||
import { db } from '$lib/data/database';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import type { LocalCollection, LocalQuestion, LocalAnswer } from './types';
|
||||
|
||||
// ─── Shared Types (inline to avoid cross-app dependency) ───
|
||||
|
|
@ -114,7 +115,9 @@ export function useAllCollections() {
|
|||
export function useAllQuestions() {
|
||||
return liveQuery(async () => {
|
||||
const locals = await db.table<LocalQuestion>('questions').toArray();
|
||||
return locals.filter((q) => !q.deletedAt).map(toQuestion);
|
||||
const visible = locals.filter((q) => !q.deletedAt);
|
||||
const decrypted = await decryptRecords('questions', visible);
|
||||
return decrypted.map(toQuestion);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +125,9 @@ export function useAllQuestions() {
|
|||
export function useAnswersByQuestion(questionId: string) {
|
||||
return liveQuery(async () => {
|
||||
const locals = await db.table<LocalAnswer>('answers').toArray();
|
||||
return locals.filter((a) => !a.deletedAt && a.questionId === questionId).map(toAnswer);
|
||||
const visible = locals.filter((a) => !a.deletedAt && a.questionId === questionId);
|
||||
const decrypted = await decryptRecords('answers', visible);
|
||||
return decrypted.map(toAnswer);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
<script lang="ts">
|
||||
import { liveQuery } from 'dexie';
|
||||
import { db } from '$lib/data/database';
|
||||
import { encryptRecord, decryptRecord } from '$lib/data/crypto';
|
||||
import { toastStore } from '@mana/shared-ui/toast';
|
||||
import { Trash } from '@mana/shared-icons';
|
||||
import type { ViewProps } from '$lib/app-registry';
|
||||
|
|
@ -32,31 +33,36 @@
|
|||
});
|
||||
|
||||
$effect(() => {
|
||||
const sub = liveQuery(() => db.table<LocalQuestion>('questions').get(questionId)).subscribe(
|
||||
(val) => {
|
||||
question = val ?? null;
|
||||
if (val && !focused) {
|
||||
editTitle = val.title;
|
||||
editDescription = val.description ?? '';
|
||||
editStatus = val.status;
|
||||
editPriority = val.priority;
|
||||
editResearchDepth = val.researchDepth;
|
||||
}
|
||||
const sub = liveQuery(async () => {
|
||||
const raw = await db.table<LocalQuestion>('questions').get(questionId);
|
||||
// title + description are encrypted on disk; decrypt a clone so
|
||||
// the inline editor binds to plaintext.
|
||||
return raw ? await decryptRecord('questions', { ...raw }) : null;
|
||||
}).subscribe((val) => {
|
||||
question = val ?? null;
|
||||
if (val && !focused) {
|
||||
editTitle = val.title;
|
||||
editDescription = val.description ?? '';
|
||||
editStatus = val.status;
|
||||
editPriority = val.priority;
|
||||
editResearchDepth = val.researchDepth;
|
||||
}
|
||||
);
|
||||
});
|
||||
return () => sub.unsubscribe();
|
||||
});
|
||||
|
||||
async function saveField() {
|
||||
focused = false;
|
||||
await db.table('questions').update(questionId, {
|
||||
const diff: Record<string, unknown> = {
|
||||
title: editTitle.trim() || question?.title || 'Ohne Titel',
|
||||
description: editDescription.trim() || undefined,
|
||||
status: editStatus,
|
||||
priority: editPriority,
|
||||
researchDepth: editResearchDepth,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
};
|
||||
await encryptRecord('questions', diff);
|
||||
await db.table('questions').update(questionId, diff);
|
||||
}
|
||||
|
||||
async function handleSelectChange() {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
<script lang="ts">
|
||||
import { liveQuery } from 'dexie';
|
||||
import { db } from '$lib/data/database';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import type { LocalLink, LocalFolder } from './types';
|
||||
import type { ViewProps } from '$lib/app-registry';
|
||||
|
||||
|
|
@ -15,10 +16,9 @@
|
|||
|
||||
$effect(() => {
|
||||
const sub = liveQuery(async () => {
|
||||
return db
|
||||
.table<LocalLink>('links')
|
||||
.toArray()
|
||||
.then((all) => all.filter((l) => !l.deletedAt && l.isActive));
|
||||
const all = await db.table<LocalLink>('links').toArray();
|
||||
const visible = all.filter((l) => !l.deletedAt && l.isActive);
|
||||
return decryptRecords('links', visible);
|
||||
}).subscribe((val) => {
|
||||
links = val ?? [];
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import { liveQuery } from 'dexie';
|
||||
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
|
||||
import { db } from '$lib/data/database';
|
||||
import { decryptRecord, decryptRecords } from '$lib/data/crypto';
|
||||
import type { LocalLink, LocalTag, LocalFolder, LocalLinkTag } from './types';
|
||||
|
||||
// ─── Shared View Types ────────────────────────────────────
|
||||
|
|
@ -133,7 +134,9 @@ export function toLinkTag(local: LocalLinkTag): LinkTag {
|
|||
export function allLinks$() {
|
||||
return liveQuery(async () => {
|
||||
const locals = await db.table<LocalLink>('links').toArray();
|
||||
return locals.filter((l) => !l.deletedAt).map(toLink);
|
||||
const visible = locals.filter((l) => !l.deletedAt);
|
||||
const decrypted = await decryptRecords('links', visible);
|
||||
return decrypted.map(toLink);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -163,7 +166,9 @@ export function allLinkTags$() {
|
|||
export function useAllLinks() {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalLink>('links').toArray();
|
||||
return locals.filter((l) => !l.deletedAt).map(toLink);
|
||||
const visible = locals.filter((l) => !l.deletedAt);
|
||||
const decrypted = await decryptRecords('links', visible);
|
||||
return decrypted.map(toLink);
|
||||
}, [] as Link[]);
|
||||
}
|
||||
|
||||
|
|
@ -194,7 +199,8 @@ export function useLinkById(id: string) {
|
|||
if (!id) return null;
|
||||
const local = await db.table<LocalLink>('links').get(id);
|
||||
if (!local || local.deletedAt) return null;
|
||||
return toLink(local);
|
||||
const decrypted = await decryptRecord('links', { ...local });
|
||||
return toLink(decrypted);
|
||||
},
|
||||
null as Link | null
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
<script lang="ts">
|
||||
import { liveQuery } from 'dexie';
|
||||
import { db } from '$lib/data/database';
|
||||
import { encryptRecord, decryptRecord } from '$lib/data/crypto';
|
||||
import { toastStore } from '@mana/shared-ui/toast';
|
||||
import { Trash } from '@mana/shared-icons';
|
||||
import type { ViewProps } from '$lib/app-registry';
|
||||
|
|
@ -33,7 +34,12 @@
|
|||
});
|
||||
|
||||
$effect(() => {
|
||||
const sub = liveQuery(() => db.table<LocalLink>('links').get(linkId)).subscribe((val) => {
|
||||
const sub = liveQuery(async () => {
|
||||
const raw = await db.table<LocalLink>('links').get(linkId);
|
||||
// title + description are encrypted on disk; decrypt a clone so
|
||||
// the inline editor binds to plaintext.
|
||||
return raw ? await decryptRecord('links', { ...raw }) : null;
|
||||
}).subscribe((val) => {
|
||||
link = val ?? null;
|
||||
if (val && !focused) {
|
||||
editTitle = val.title ?? '';
|
||||
|
|
@ -49,7 +55,7 @@
|
|||
|
||||
async function saveField() {
|
||||
focused = false;
|
||||
await db.table('links').update(linkId, {
|
||||
const diff: Record<string, unknown> = {
|
||||
title: editTitle.trim() || undefined,
|
||||
originalUrl: editOriginalUrl.trim() || link?.originalUrl || '',
|
||||
customCode: editCustomCode.trim() || undefined,
|
||||
|
|
@ -57,7 +63,9 @@
|
|||
isActive: editIsActive,
|
||||
expiresAt: editExpiresAt ? new Date(editExpiresAt).toISOString() : null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
};
|
||||
await encryptRecord('links', diff);
|
||||
await db.table('links').update(linkId, diff);
|
||||
}
|
||||
|
||||
async function handleActiveToggle() {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@
|
|||
getAllDocumentTags,
|
||||
} from '$lib/modules/context/queries';
|
||||
import { documentTable } from '$lib/modules/context/collections';
|
||||
import type { DocumentType } from '$lib/modules/context/types';
|
||||
import { encryptRecord } from '$lib/data/crypto';
|
||||
import type { DocumentType, LocalDocument } from '$lib/modules/context/types';
|
||||
|
||||
let searchQuery = $state('');
|
||||
let typeFilter = $state<DocumentType | 'all'>('all');
|
||||
|
|
@ -32,7 +33,7 @@
|
|||
|
||||
async function handleCreateDocument() {
|
||||
const id = crypto.randomUUID();
|
||||
await documentTable.add({
|
||||
const row: LocalDocument = {
|
||||
id,
|
||||
spaceId: null,
|
||||
title: 'Neues Dokument',
|
||||
|
|
@ -41,7 +42,9 @@
|
|||
shortId: null,
|
||||
pinned: false,
|
||||
metadata: null,
|
||||
});
|
||||
};
|
||||
await encryptRecord('documents', row);
|
||||
await documentTable.add(row);
|
||||
goto(`/context/documents/${id}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import { ArrowLeft, Trash } from '@mana/shared-icons';
|
||||
import { useAllDocuments, findDocumentById } from '$lib/modules/context/queries';
|
||||
import { documentTable } from '$lib/modules/context/collections';
|
||||
import { encryptRecord } from '$lib/data/crypto';
|
||||
import type { DocumentType } from '$lib/modules/context/types';
|
||||
|
||||
let showDeleteConfirm = $state(false);
|
||||
|
|
@ -47,7 +48,7 @@
|
|||
.map((t) => t.trim())
|
||||
.filter(Boolean);
|
||||
const wordCount = editContent.split(/\s+/).filter(Boolean).length;
|
||||
await documentTable.update(docId, {
|
||||
const diff: Record<string, unknown> = {
|
||||
title: editTitle,
|
||||
content: editContent,
|
||||
type: editType,
|
||||
|
|
@ -56,7 +57,9 @@
|
|||
tags,
|
||||
word_count: wordCount,
|
||||
},
|
||||
});
|
||||
};
|
||||
await encryptRecord('documents', diff);
|
||||
await documentTable.update(docId, diff);
|
||||
saving = false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@
|
|||
findSpaceById,
|
||||
} from '$lib/modules/context/queries';
|
||||
import { contextSpaceTable, documentTable } from '$lib/modules/context/collections';
|
||||
import type { DocumentType } from '$lib/modules/context/types';
|
||||
import { encryptRecord } from '$lib/data/crypto';
|
||||
import type { DocumentType, LocalDocument } from '$lib/modules/context/types';
|
||||
|
||||
let editingName = $state(false);
|
||||
let editName = $state('');
|
||||
|
|
@ -39,7 +40,7 @@
|
|||
|
||||
async function handleCreateDocument() {
|
||||
const id = crypto.randomUUID();
|
||||
await documentTable.add({
|
||||
const row: LocalDocument = {
|
||||
id,
|
||||
spaceId,
|
||||
title: 'Neues Dokument',
|
||||
|
|
@ -48,7 +49,9 @@
|
|||
shortId: null,
|
||||
pinned: false,
|
||||
metadata: null,
|
||||
});
|
||||
};
|
||||
await encryptRecord('documents', row);
|
||||
await documentTable.add(row);
|
||||
goto(`/context/documents/${id}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { _ } from 'svelte-i18n';
|
||||
import { goto } from '$app/navigation';
|
||||
import { db } from '$lib/data/database';
|
||||
import { encryptRecord } from '$lib/data/crypto';
|
||||
import { useAllFavorites } from '$lib/modules/nutriphi/queries';
|
||||
import { MEAL_TYPE_LABELS, suggestMealType } from '$lib/modules/nutriphi/constants';
|
||||
import type { MealType, NutritionData } from '$lib/modules/nutriphi/types';
|
||||
|
|
@ -64,7 +65,7 @@
|
|||
}
|
||||
: null;
|
||||
|
||||
await db.table('meals').add({
|
||||
const row: Record<string, unknown> = {
|
||||
id: crypto.randomUUID(),
|
||||
date: today,
|
||||
mealType,
|
||||
|
|
@ -75,7 +76,9 @@
|
|||
nutrition,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
};
|
||||
await encryptRecord('meals', row);
|
||||
await db.table('meals').add(row);
|
||||
|
||||
goto('/nutriphi');
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { _ } from 'svelte-i18n';
|
||||
import { page } from '$app/stores';
|
||||
import { db } from '$lib/data/database';
|
||||
import { encryptRecord } from '$lib/data/crypto';
|
||||
import {
|
||||
useAllQuestions,
|
||||
useAnswersByQuestion,
|
||||
|
|
@ -67,11 +68,13 @@
|
|||
|
||||
async function saveEdit() {
|
||||
if (!question || !editTitle.trim()) return;
|
||||
await db.table('questions').update(question.id, {
|
||||
const diff: Record<string, unknown> = {
|
||||
title: editTitle.trim(),
|
||||
description: editDescription.trim() || null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
};
|
||||
await encryptRecord('questions', diff);
|
||||
await db.table('questions').update(question.id, diff);
|
||||
editing = false;
|
||||
}
|
||||
|
||||
|
|
@ -98,7 +101,7 @@
|
|||
|
||||
try {
|
||||
const now = new Date().toISOString();
|
||||
await db.table('answers').add({
|
||||
const row: Record<string, unknown> = {
|
||||
id: crypto.randomUUID(),
|
||||
questionId: question.id,
|
||||
researchResultId: null,
|
||||
|
|
@ -108,7 +111,9 @@
|
|||
isAccepted: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
};
|
||||
await encryptRecord('answers', row);
|
||||
await db.table('answers').add(row);
|
||||
newAnswer = '';
|
||||
|
||||
// Mark question as answered if it was open
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { _ } from 'svelte-i18n';
|
||||
import { goto } from '$app/navigation';
|
||||
import { db } from '$lib/data/database';
|
||||
import { encryptRecord } from '$lib/data/crypto';
|
||||
import { useAllCollections } from '$lib/modules/questions/queries';
|
||||
import type { ResearchDepth, QuestionPriority } from '$lib/modules/questions/types';
|
||||
import { ArrowLeft, Lightning, Clock, Sparkle } from '@mana/shared-icons';
|
||||
|
|
@ -63,7 +64,7 @@
|
|||
const now = new Date().toISOString();
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
await db.table('questions').add({
|
||||
const row: Record<string, unknown> = {
|
||||
id,
|
||||
collectionId: collectionId || null,
|
||||
title: title.trim(),
|
||||
|
|
@ -74,7 +75,9 @@
|
|||
researchDepth,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
};
|
||||
await encryptRecord('questions', row);
|
||||
await db.table('questions').add(row);
|
||||
|
||||
goto(`/questions/${id}`);
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
type StatusFilter,
|
||||
} from '$lib/modules/uload/queries';
|
||||
import { linkTable, uloadFolderTable } from '$lib/modules/uload/collections';
|
||||
import { encryptRecord } from '$lib/data/crypto';
|
||||
import type { LocalLink } from '$lib/modules/uload/types';
|
||||
import {
|
||||
CaretRight,
|
||||
|
|
@ -141,7 +142,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
await linkTable.add({
|
||||
const newRow: LocalLink = {
|
||||
id: crypto.randomUUID(),
|
||||
shortCode,
|
||||
customCode: newCustomCode || null,
|
||||
|
|
@ -159,7 +160,9 @@
|
|||
utmCampaign: newUtmCampaign || null,
|
||||
folderId: selectedFolderId,
|
||||
order: links.length,
|
||||
} satisfies LocalLink);
|
||||
};
|
||||
await encryptRecord('links', newRow);
|
||||
await linkTable.add(newRow);
|
||||
toast.success(`Link erstellt: ${shortCode}`);
|
||||
newUrl = '';
|
||||
newTitle = '';
|
||||
|
|
@ -205,7 +208,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
await linkTable.update(editingLink.id, {
|
||||
const diff: Record<string, unknown> = {
|
||||
originalUrl: editUrl,
|
||||
title: editTitle || null,
|
||||
utmSource: editUtmSource || null,
|
||||
|
|
@ -214,7 +217,9 @@
|
|||
expiresAt: editExpiresAt || null,
|
||||
password: editPassword || null,
|
||||
maxClicks,
|
||||
});
|
||||
};
|
||||
await encryptRecord('links', diff);
|
||||
await linkTable.update(editingLink.id, diff);
|
||||
toast.success('Link aktualisiert');
|
||||
editingLink = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { ArrowLeft, Trash, DownloadSimple } from '@mana/shared-icons';
|
||||
import { linkTable, uloadTagTable, uloadFolderTable, linkTagTable } from '$lib/modules/uload';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import { useAllLinks, useAllTags, useAllFolders } from '$lib/modules/uload';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
|
|
@ -20,7 +21,10 @@
|
|||
}
|
||||
|
||||
async function exportData() {
|
||||
const allLinks = await linkTable.toArray();
|
||||
// Decrypt links before serializing the export — otherwise the user
|
||||
// would download a JSON of ciphertext blobs they couldn't restore.
|
||||
const rawLinks = await linkTable.toArray();
|
||||
const allLinks = await decryptRecords('links', rawLinks);
|
||||
const allTags = await uloadTagTable.toArray();
|
||||
const allFolders = await uloadFolderTable.toArray();
|
||||
const allLinkTags = await linkTagTable.toArray();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue