mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-23 15:09:33 +01:00
core: add support for monographs sync
This commit is contained in:
@@ -54,6 +54,29 @@ function PublishView(props: PublishViewProps) {
|
||||
const publishNote = useStore((store) => store.publish);
|
||||
const unpublishNote = useStore((store) => store.unpublish);
|
||||
|
||||
useEffect(() => {
|
||||
if (!publishId) return;
|
||||
(async () => {
|
||||
const monographId = db.monographs.monograph(note.id);
|
||||
if (monographId) {
|
||||
const monograph = await db.monographs.get(monographId);
|
||||
if (!monograph) return;
|
||||
setPublishId(monographId);
|
||||
setIsPasswordProtected(!!monograph.password);
|
||||
setSelfDestruct(!!monograph.selfDestruct);
|
||||
|
||||
if (monograph.password) {
|
||||
const password = await db.monographs.decryptPassword(
|
||||
monograph.password
|
||||
);
|
||||
if (passwordInput.current) {
|
||||
passwordInput.current.value = password;
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [publishId, isPublishing]);
|
||||
|
||||
useEffect(() => {
|
||||
const fileDownloadedEvent = EV.subscribe(
|
||||
EVENTS.fileDownloaded,
|
||||
@@ -110,7 +133,13 @@ function PublishView(props: PublishViewProps) {
|
||||
) : (
|
||||
<>
|
||||
{publishId ? (
|
||||
<Flex mt={1} sx={{ flexDirection: "column", overflow: "hidden" }}>
|
||||
<Flex
|
||||
mt={1}
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{ fontWeight: "bold", color: "paragraph" }}
|
||||
@@ -154,7 +183,12 @@ function PublishView(props: PublishViewProps) {
|
||||
</Flex>
|
||||
</Flex>
|
||||
) : (
|
||||
<Text variant="body" sx={{ color: "paragraph" }}>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{
|
||||
color: "paragraph"
|
||||
}}
|
||||
>
|
||||
{strings.monographDesc()}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -36,6 +36,7 @@ import Migrations from "./migrations.js";
|
||||
import UserManager from "./user-manager.js";
|
||||
import http from "../utils/http.js";
|
||||
import { Monographs } from "./monographs.js";
|
||||
import { Monographs as MonographsCollection } from "../collections/monographs.js";
|
||||
import { Offers } from "./offers.js";
|
||||
import { Attachments } from "../collections/attachments.js";
|
||||
import { Debug } from "./debug.js";
|
||||
@@ -203,6 +204,7 @@ class Database {
|
||||
trash = new Trash(this);
|
||||
sanitizer = new Sanitizer(this.sql);
|
||||
|
||||
monographsCollection = new MonographsCollection(this);
|
||||
notebooks = new Notebooks(this);
|
||||
tags = new Tags(this);
|
||||
colors = new Colors(this);
|
||||
@@ -329,6 +331,7 @@ class Database {
|
||||
await this.relations.init();
|
||||
await this.notes.init();
|
||||
await this.vaults.init();
|
||||
await this.monographsCollection.init();
|
||||
|
||||
await this.trash.init();
|
||||
|
||||
@@ -407,6 +410,9 @@ class Database {
|
||||
EV.publish(EVENTS.userEmailConfirmed);
|
||||
break;
|
||||
}
|
||||
case "triggerSync": {
|
||||
await this.sync({ type: "fetch" });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("SSE: Unsupported message. Message = ", event.data);
|
||||
|
||||
@@ -20,24 +20,23 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import http from "../utils/http.js";
|
||||
import Constants from "../utils/constants.js";
|
||||
import Database from "./index.js";
|
||||
import { Note, isDeleted } from "../types.js";
|
||||
import { Monograph, Note, isDeleted } from "../types.js";
|
||||
import { Cipher } from "@notesnook/crypto";
|
||||
import { isFalse } from "../database/index.js";
|
||||
import { logger } from "../logger.js";
|
||||
|
||||
type BaseMonograph = {
|
||||
id: string;
|
||||
title: string;
|
||||
userId: string;
|
||||
selfDestruct: boolean;
|
||||
};
|
||||
type UnencryptedMonograph = BaseMonograph & {
|
||||
type MonographApiRequestBase = Omit<
|
||||
Monograph,
|
||||
"type" | "dateModified" | "dateCreated" | "datePublished"
|
||||
>;
|
||||
type UnencryptedMonograph = MonographApiRequestBase & {
|
||||
content: string;
|
||||
};
|
||||
type EncryptedMonograph = BaseMonograph & {
|
||||
type EncryptedMonograph = MonographApiRequestBase & {
|
||||
encryptedContent: Cipher<"base64">;
|
||||
};
|
||||
type Monograph = UnencryptedMonograph | EncryptedMonograph;
|
||||
type MonographApiRequest = (UnencryptedMonograph | EncryptedMonograph) & {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export type PublishOptions = { password?: string; selfDestruct?: boolean };
|
||||
export class Monographs {
|
||||
@@ -46,24 +45,12 @@ export class Monographs {
|
||||
|
||||
async clear() {
|
||||
this.monographs = [];
|
||||
await this.db.kv().write("monographs", this.monographs);
|
||||
await this.db.monographsCollection.collection.clear();
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
try {
|
||||
const user = await this.db.user.getUser();
|
||||
const token = await this.db.tokenManager.getAccessToken();
|
||||
if (!user || !token || !user.isEmailConfirmed) return;
|
||||
|
||||
const monographs = await http.get(
|
||||
`${Constants.API_HOST}/monographs`,
|
||||
token
|
||||
);
|
||||
await this.db.kv().write("monographs", monographs);
|
||||
if (monographs) this.monographs = monographs;
|
||||
} catch (e) {
|
||||
logger.error(e, "Error while refreshing monographs.");
|
||||
}
|
||||
const ids = await this.db.monographsCollection.all.ids();
|
||||
this.monographs = ids;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,13 +96,19 @@ export class Monographs {
|
||||
false
|
||||
);
|
||||
|
||||
const monograph: Monograph = {
|
||||
const monographPasswordsKey = await this.db.user.getMonographPasswordsKey();
|
||||
const monograph: MonographApiRequest = {
|
||||
id: noteId,
|
||||
title: note.title,
|
||||
userId: user.id,
|
||||
selfDestruct: opts.selfDestruct || false,
|
||||
...(opts.password
|
||||
? {
|
||||
password: monographPasswordsKey
|
||||
? await this.db
|
||||
.storage()
|
||||
.encrypt(monographPasswordsKey, opts.password)
|
||||
: undefined,
|
||||
encryptedContent: await this.db
|
||||
.storage()
|
||||
.encrypt(
|
||||
@@ -124,6 +117,7 @@ export class Monographs {
|
||||
)
|
||||
}
|
||||
: {
|
||||
password: undefined,
|
||||
content: JSON.stringify({
|
||||
type: content.type,
|
||||
data: content.data
|
||||
@@ -132,14 +126,21 @@ export class Monographs {
|
||||
};
|
||||
|
||||
const method = update ? http.patch.json : http.post.json;
|
||||
|
||||
const { id } = await method(
|
||||
`${Constants.API_HOST}/monographs`,
|
||||
const deviceId = await this.db.kv().read("deviceId");
|
||||
const { id, datePublished } = await method(
|
||||
`${Constants.API_HOST}/monographs?deviceId=${deviceId}`,
|
||||
monograph,
|
||||
token
|
||||
);
|
||||
|
||||
this.monographs.push(id);
|
||||
await this.db.monographsCollection.add({
|
||||
id,
|
||||
title: monograph.title,
|
||||
selfDestruct: monograph.selfDestruct,
|
||||
datePublished: datePublished,
|
||||
password: monograph.password
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -156,9 +157,14 @@ export class Monographs {
|
||||
if (!this.isPublished(noteId))
|
||||
throw new Error("This note is not published.");
|
||||
|
||||
await http.delete(`${Constants.API_HOST}/monographs/${noteId}`, token);
|
||||
const deviceId = await this.db.kv().read("deviceId");
|
||||
await http.delete(
|
||||
`${Constants.API_HOST}/monographs/${noteId}?deviceId=${deviceId}`,
|
||||
token
|
||||
);
|
||||
|
||||
this.monographs.splice(this.monographs.indexOf(noteId), 1);
|
||||
await this.db.monographsCollection.collection.softDelete([noteId]);
|
||||
}
|
||||
|
||||
get all() {
|
||||
@@ -173,6 +179,12 @@ export class Monographs {
|
||||
}
|
||||
|
||||
get(monographId: string) {
|
||||
return http.get(`${Constants.API_HOST}/monographs/${monographId}`);
|
||||
return this.db.monographsCollection.collection.get(monographId);
|
||||
}
|
||||
|
||||
async decryptPassword(password: Cipher<"base64">) {
|
||||
const monographPasswordsKey = await this.db.user.getMonographPasswordsKey();
|
||||
if (!monographPasswordsKey) return "";
|
||||
return this.db.storage().decrypt(monographPasswordsKey, password);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
isTrashItem,
|
||||
Item,
|
||||
MaybeDeletedItem,
|
||||
Monograph,
|
||||
Note,
|
||||
Notebook
|
||||
} from "../../types.js";
|
||||
@@ -53,6 +54,7 @@ import {
|
||||
import { DownloadableFile } from "../../database/fs.js";
|
||||
import { SyncDevices } from "./devices.js";
|
||||
import { DefaultColors } from "../../collections/colors.js";
|
||||
import { Monographs } from "../monographs.js";
|
||||
|
||||
enum LogLevel {
|
||||
/** Log level for very low severity diagnostic messages. */
|
||||
@@ -463,6 +465,19 @@ class Sync {
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
this.connection.on("SendMonographs", async (monographs) => {
|
||||
if (this.connection?.state !== HubConnectionState.Connected) return false;
|
||||
|
||||
this.db.monographsCollection.collection.put(
|
||||
monographs.map((m: Monograph) => ({
|
||||
...m,
|
||||
type: "monograph"
|
||||
}))
|
||||
);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private async getKey() {
|
||||
|
||||
@@ -43,6 +43,7 @@ const ENDPOINTS = {
|
||||
class UserManager {
|
||||
private tokenManager: TokenManager;
|
||||
private cachedAttachmentKey?: SerializedKey;
|
||||
private cachedMonographPasswordsKey?: SerializedKey;
|
||||
constructor(private readonly db: Database) {
|
||||
this.tokenManager = new TokenManager(this.db.kv);
|
||||
|
||||
@@ -459,6 +460,51 @@ class UserManager {
|
||||
}
|
||||
}
|
||||
|
||||
async getMonographPasswordsKey() {
|
||||
if (this.cachedMonographPasswordsKey) {
|
||||
return this.cachedMonographPasswordsKey;
|
||||
}
|
||||
|
||||
try {
|
||||
let user = await this.getUser();
|
||||
if (!user) return;
|
||||
|
||||
if (!user.monographPasswordsKey) {
|
||||
const token = await this.tokenManager.getAccessToken();
|
||||
user = await http.get(`${constants.API_HOST}${ENDPOINTS.user}`, token);
|
||||
}
|
||||
if (!user) return;
|
||||
|
||||
const userEncryptionKey = await this.getEncryptionKey();
|
||||
if (!userEncryptionKey) return;
|
||||
|
||||
if (!user.monographPasswordsKey) {
|
||||
const key = await this.db.crypto().generateRandomKey();
|
||||
user.monographPasswordsKey = await this.db
|
||||
.storage()
|
||||
.encrypt(userEncryptionKey, JSON.stringify(key));
|
||||
|
||||
await this.updateUser({
|
||||
monographPasswordsKey: user.monographPasswordsKey
|
||||
});
|
||||
return key;
|
||||
}
|
||||
|
||||
const plainData = await this.db
|
||||
.storage()
|
||||
.decrypt(userEncryptionKey, user.monographPasswordsKey);
|
||||
if (!plainData) return;
|
||||
this.cachedMonographPasswordsKey = JSON.parse(plainData) as SerializedKey;
|
||||
return this.cachedMonographPasswordsKey;
|
||||
} catch (e) {
|
||||
logger.error(e, "Could not get monographs encryption key.");
|
||||
if (e instanceof Error)
|
||||
throw new Error(
|
||||
`Could not get monographs encryption key. Error: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async sendVerificationEmail(newEmail?: string) {
|
||||
const token = await this.tokenManager.getAccessToken();
|
||||
if (!token) return;
|
||||
@@ -533,7 +579,6 @@ class UserManager {
|
||||
|
||||
if (!new_password) throw new Error("New password is required.");
|
||||
|
||||
const attachmentsKey = await this.getAttachmentsKey();
|
||||
data.encryptionKey = data.encryptionKey || (await this.getEncryptionKey());
|
||||
|
||||
await this.clearSessions();
|
||||
@@ -554,13 +599,26 @@ class UserManager {
|
||||
|
||||
await this.db.sync({ type: "send", force: true });
|
||||
|
||||
if (attachmentsKey) {
|
||||
const userEncryptionKey = await this.getEncryptionKey();
|
||||
if (!userEncryptionKey) return;
|
||||
if (userEncryptionKey) {
|
||||
const updateUserPayload: Partial<User> = {};
|
||||
const attachmentsKey = await this.getAttachmentsKey();
|
||||
if (attachmentsKey) {
|
||||
user.attachmentsKey = await this.db
|
||||
.storage()
|
||||
.encrypt(userEncryptionKey, JSON.stringify(attachmentsKey));
|
||||
await this.updateUser({ attachmentsKey: user.attachmentsKey });
|
||||
updateUserPayload.attachmentsKey = user.attachmentsKey;
|
||||
}
|
||||
const monographPasswordsKey = await this.getMonographPasswordsKey();
|
||||
if (monographPasswordsKey) {
|
||||
user.monographPasswordsKey = await this.db
|
||||
.storage()
|
||||
.encrypt(userEncryptionKey, JSON.stringify(monographPasswordsKey));
|
||||
updateUserPayload.monographPasswordsKey = user.monographPasswordsKey;
|
||||
}
|
||||
if (Object.keys(updateUserPayload).length > 0) {
|
||||
await this.updateUser(updateUserPayload);
|
||||
}
|
||||
}
|
||||
|
||||
if (new_password)
|
||||
|
||||
69
packages/core/src/collections/monographs.ts
Normal file
69
packages/core/src/collections/monographs.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Database from "../api/index.js";
|
||||
import { ItemReference, Monograph, Vault } from "../types.js";
|
||||
import { ICollection } from "./collection.js";
|
||||
import { SQLCollection } from "../database/sql-collection.js";
|
||||
import { getId } from "../utils/id.js";
|
||||
import { isFalse } from "../database/index.js";
|
||||
import { sql } from "@streetwriters/kysely";
|
||||
|
||||
export class Monographs implements ICollection {
|
||||
name = "monographs";
|
||||
readonly collection: SQLCollection<"monographs", Monograph>;
|
||||
constructor(private readonly db: Database) {
|
||||
this.collection = new SQLCollection(
|
||||
db.sql,
|
||||
db.transaction,
|
||||
"monographs",
|
||||
db.eventManager,
|
||||
db.sanitizer
|
||||
);
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.collection.init();
|
||||
}
|
||||
|
||||
get all() {
|
||||
return this.collection.createFilter<Monograph>(
|
||||
(qb) => qb.where(isFalse("deleted")),
|
||||
this.db.options?.batchSize
|
||||
);
|
||||
}
|
||||
|
||||
async add(monograph: Partial<Monograph>) {
|
||||
const id = monograph.id || getId();
|
||||
const oldMonograph = await this.collection.get(id);
|
||||
const merged: Partial<Monograph> = {
|
||||
...oldMonograph,
|
||||
...monograph
|
||||
};
|
||||
|
||||
this.collection.upsert({
|
||||
id,
|
||||
title: merged.title,
|
||||
datePublished: merged.datePublished,
|
||||
selfDestruct: merged.selfDestruct,
|
||||
password: merged.password,
|
||||
type: "monograph"
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
ItemReferences,
|
||||
ItemType,
|
||||
MaybeDeletedItem,
|
||||
Monograph,
|
||||
Note,
|
||||
Notebook,
|
||||
Relation,
|
||||
@@ -90,6 +91,7 @@ export interface DatabaseSchema {
|
||||
sessioncontent: SQLiteItem<SessionContentItem>;
|
||||
shortcuts: SQLiteItem<Shortcut>;
|
||||
vaults: SQLiteItem<Vault>;
|
||||
monographs: SQLiteItem<Monograph>;
|
||||
}
|
||||
|
||||
export type RawDatabaseSchema = DatabaseSchema & {
|
||||
@@ -233,7 +235,8 @@ const BooleanProperties: Set<BooleanFields> = new Set([
|
||||
"remote",
|
||||
"synced",
|
||||
"isGeneratedTitle",
|
||||
"archived"
|
||||
"archived",
|
||||
"selfDestruct"
|
||||
]);
|
||||
|
||||
const DataMappers: Partial<Record<ItemType, (row: any) => void>> = {
|
||||
@@ -266,6 +269,9 @@ const DataMappers: Partial<Record<ItemType, (row: any) => void>> = {
|
||||
},
|
||||
vault: (row) => {
|
||||
if (row.key) row.key = JSON.parse(row.key);
|
||||
},
|
||||
monograph: (row) => {
|
||||
if (row.password) row.password = JSON.parse(row.password);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -392,6 +392,18 @@ export class NNMigrationProvider implements MigrationProvider {
|
||||
async up(db) {
|
||||
await runFTSTablesMigrations(db);
|
||||
}
|
||||
},
|
||||
"a-2025-07-30": {
|
||||
async up(db) {
|
||||
await db.schema
|
||||
.createTable("monographs")
|
||||
.$call(addBaseColumns)
|
||||
.addColumn("datePublished", "integer")
|
||||
.addColumn("title", "text", COLLATE_NOCASE)
|
||||
.addColumn("selfDestruct", "boolean")
|
||||
.addColumn("password", "text")
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -701,7 +701,8 @@ const VALID_SORT_OPTIONS: Record<
|
||||
sessioncontent: [],
|
||||
settings: [],
|
||||
shortcuts: [],
|
||||
vaults: []
|
||||
vaults: [],
|
||||
monographs: []
|
||||
};
|
||||
|
||||
function sanitizeSortOptions(type: keyof DatabaseSchema, options: SortOptions) {
|
||||
|
||||
@@ -80,6 +80,7 @@ export type Collections = {
|
||||
sessioncontent: "sessioncontent";
|
||||
settingsv2: "settingitem";
|
||||
vaults: "vault";
|
||||
monographs: "monograph";
|
||||
|
||||
/**
|
||||
* @deprecated only kept here for migration purposes
|
||||
@@ -110,6 +111,7 @@ export type GroupableItem = ValueOf<
|
||||
| "settings"
|
||||
| "settingitem"
|
||||
| "vault"
|
||||
| "monograph"
|
||||
>
|
||||
>;
|
||||
|
||||
@@ -131,6 +133,7 @@ export type ItemMap = {
|
||||
settingitem: SettingItem;
|
||||
vault: Vault;
|
||||
searchResult: HighlightedResult;
|
||||
monograph: Monograph;
|
||||
|
||||
/**
|
||||
* @deprecated only kept here for migration purposes
|
||||
@@ -490,6 +493,13 @@ export interface Vault extends BaseItem<"vault"> {
|
||||
key: Cipher<"base64">;
|
||||
}
|
||||
|
||||
export interface Monograph extends BaseItem<"monograph"> {
|
||||
title: string;
|
||||
datePublished: number;
|
||||
selfDestruct: boolean;
|
||||
password?: Cipher<"base64">;
|
||||
}
|
||||
|
||||
export type Match = {
|
||||
prefix: string;
|
||||
match: string;
|
||||
@@ -546,6 +556,7 @@ export type User = {
|
||||
isEmailConfirmed: boolean;
|
||||
salt: string;
|
||||
attachmentsKey?: Cipher<"base64">;
|
||||
monographPasswordsKey?: Cipher<"base64">;
|
||||
marketingConsent?: boolean;
|
||||
mfa: {
|
||||
isEnabled: boolean;
|
||||
|
||||
Reference in New Issue
Block a user