Better code seperation for backend
This commit is contained in:
@@ -1,346 +0,0 @@
|
||||
import sodium from 'libsodium-wrappers-sumo';
|
||||
import { rawMasterKeyStore } from '../store';
|
||||
import { get } from 'svelte/store';
|
||||
import { parseChannelRSS } from './youtubejs/subscriptions';
|
||||
import { getChannelYTjs } from './youtubejs/channel';
|
||||
import type { ChannelSubscriptions } from '$lib/dexie';
|
||||
import type { VideoPlay } from './model';
|
||||
import { vi } from 'zod/v4/locales';
|
||||
import { getBestThumbnail } from '$lib/images';
|
||||
|
||||
async function getSecureHash(toHash: string, rawKey: Uint8Array): Promise<string> {
|
||||
await sodium.ready;
|
||||
return sodium.to_base64(
|
||||
sodium.crypto_generichash(sodium.crypto_generichash_BYTES, toHash, rawKey)
|
||||
);
|
||||
}
|
||||
|
||||
async function getRawKey(): Promise<Uint8Array | undefined> {
|
||||
const rawMasterKey = get(rawMasterKeyStore);
|
||||
if (!rawMasterKey) return;
|
||||
|
||||
await sodium.ready;
|
||||
|
||||
return sodium.from_base64(rawMasterKey);
|
||||
}
|
||||
|
||||
export async function getSubscriptionsBackend(): Promise<ChannelSubscriptions[]> {
|
||||
const resp = await fetch(`/api/user/subscriptions`, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!resp.ok) return [];
|
||||
|
||||
const subscriptions: ChannelSubscriptions[] = [];
|
||||
|
||||
const respJson = await resp.json();
|
||||
|
||||
for (const sub of respJson.subscriptions) {
|
||||
subscriptions.push({
|
||||
channelName: (await decryptWithMasterKey(sub.channelNameNonce, sub.channelNameCipher)) ?? '',
|
||||
channelId: (await decryptWithMasterKey(sub.channelIdNonce, sub.channelIdCipher)) ?? '',
|
||||
lastRSSFetch: new Date(sub.lastRSSFetch)
|
||||
});
|
||||
}
|
||||
|
||||
return subscriptions;
|
||||
}
|
||||
|
||||
export async function updateRSSLastUpdated(authorId: string) {
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return false;
|
||||
|
||||
const internalAuthorId = await getSecureHash(authorId, rawKey);
|
||||
|
||||
await fetch(`/api/user/subscriptions/${internalAuthorId}`, {
|
||||
method: 'PATCH',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
}
|
||||
|
||||
export async function amSubscribedBackend(authorId: string): Promise<boolean> {
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return false;
|
||||
|
||||
const internalAuthorId = await getSecureHash(authorId, rawKey);
|
||||
|
||||
const resp = await fetch(`/api/user/subscriptions/${internalAuthorId}`, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
if (!resp.ok) return false;
|
||||
|
||||
const respJson = await resp.json();
|
||||
|
||||
return respJson.amSubscribed;
|
||||
}
|
||||
|
||||
export async function deleteUnsubscribeBackend(authorId: string) {
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return false;
|
||||
|
||||
const internalAuthorId = await getSecureHash(authorId, rawKey);
|
||||
|
||||
await fetch(`/api/user/subscriptions/${internalAuthorId}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
}
|
||||
|
||||
export async function postSubscribeBackend(
|
||||
authorId: string,
|
||||
authorName: string | undefined = undefined
|
||||
) {
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return;
|
||||
|
||||
const internalAuthorId = await getSecureHash(authorId, rawKey);
|
||||
|
||||
if (!authorName) {
|
||||
const channel = await getChannelYTjs(authorId);
|
||||
authorName = channel.author;
|
||||
}
|
||||
|
||||
const channelId = await encryptWithMasterKey(authorId);
|
||||
const channelName = await encryptWithMasterKey(authorName);
|
||||
|
||||
const resp = await fetch(`/api/user/subscriptions/${internalAuthorId}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
channelIdCipher: channelId?.cipher,
|
||||
channelIdNonce: channelId?.nonce,
|
||||
channelNameCipher: channelName?.cipher,
|
||||
channelNameNonce: channelName?.nonce
|
||||
}),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (resp.ok) parseChannelRSS(authorId);
|
||||
}
|
||||
|
||||
export type DerivePassword = (rawPassword: string, passwordSalt: Uint8Array) => Promise<Uint8Array>;
|
||||
|
||||
export async function createUserBackend(
|
||||
username: string,
|
||||
rawPassword: string,
|
||||
captchaPayload: string,
|
||||
derivePassword: DerivePassword
|
||||
): Promise<boolean> {
|
||||
await sodium.ready;
|
||||
|
||||
const passwordSalt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
|
||||
|
||||
const loginHash = await derivePassword(rawPassword, passwordSalt);
|
||||
|
||||
const decryptionKeySalt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
|
||||
const rawDecryptionKey = sodium.crypto_pwhash(
|
||||
32,
|
||||
rawPassword,
|
||||
decryptionKeySalt,
|
||||
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_ALG_DEFAULT
|
||||
);
|
||||
|
||||
const rawDecryptionMasterKey = sodium.randombytes_buf(sodium.crypto_secretbox_KEYBYTES);
|
||||
const decryptionMasterKeyNonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
||||
|
||||
const masterKeyCipher = sodium.crypto_secretbox_easy(
|
||||
rawDecryptionMasterKey,
|
||||
decryptionMasterKeyNonce,
|
||||
rawDecryptionKey
|
||||
);
|
||||
|
||||
const userCreateResp = await fetch('/api/user/create', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: {
|
||||
hash: sodium.to_base64(loginHash),
|
||||
salt: sodium.to_base64(passwordSalt)
|
||||
},
|
||||
decryptionKeySalt: sodium.to_base64(decryptionKeySalt),
|
||||
masterKey: {
|
||||
cipher: sodium.to_base64(masterKeyCipher),
|
||||
nonce: sodium.to_base64(decryptionMasterKeyNonce)
|
||||
},
|
||||
captchaPayload
|
||||
}),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!userCreateResp.ok) return false;
|
||||
|
||||
rawMasterKeyStore.set(sodium.to_base64(rawDecryptionMasterKey));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function loginUserBackend(
|
||||
username: string,
|
||||
rawPassword: string,
|
||||
captchaPayload: string,
|
||||
derivePassword: DerivePassword
|
||||
): Promise<boolean> {
|
||||
await sodium.ready;
|
||||
|
||||
const passwordSaltsResp = await fetch(`/api/user/${username}/public`);
|
||||
if (!passwordSaltsResp.ok) return false;
|
||||
|
||||
const passwordSalts = await passwordSaltsResp.json();
|
||||
|
||||
const loginHash = await derivePassword(
|
||||
rawPassword,
|
||||
sodium.from_base64(passwordSalts.passwordSalt)
|
||||
);
|
||||
|
||||
const loginResp = await fetch('/api/user/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
passwordHash: sodium.to_base64(loginHash),
|
||||
captchaPayload
|
||||
}),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!loginResp.ok) return false;
|
||||
|
||||
const loginJson = await loginResp.json();
|
||||
|
||||
const rawDecryptionKey = sodium.crypto_pwhash(
|
||||
sodium.crypto_secretbox_KEYBYTES,
|
||||
rawPassword,
|
||||
sodium.from_base64(passwordSalts.decryptionKeySalt),
|
||||
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_ALG_DEFAULT
|
||||
);
|
||||
|
||||
const rawDecryptionMasterKey = sodium.crypto_secretbox_open_easy(
|
||||
sodium.from_base64(loginJson.masterKeyCipher),
|
||||
sodium.from_base64(loginJson.masterKeyNonce),
|
||||
rawDecryptionKey
|
||||
);
|
||||
|
||||
rawMasterKeyStore.set(sodium.to_base64(rawDecryptionMasterKey));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function addOrUpdateKeyValue(key: string, value: string) {
|
||||
const valueEncrypted = await encryptWithMasterKey(value);
|
||||
|
||||
await fetch(`/api/user/keyValue/${key}`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
valueCipher: valueEncrypted?.cipher,
|
||||
valueNonce: valueEncrypted?.nonce
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteKeyValue(key: string) {
|
||||
await fetch(`/api/user/keyValue/${key}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
}
|
||||
|
||||
export type KeyValue = boolean | number | string[] | string | object | undefined;
|
||||
|
||||
export async function getKeyValue(key: string): Promise<KeyValue | null> {
|
||||
const resp = await fetch(`/api/user/keyValue/${key}`, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!resp.ok) return null;
|
||||
|
||||
const respJson = await resp.json();
|
||||
return (await decryptWithMasterKey(respJson.valueNonce, respJson.valueCipher)) ?? null;
|
||||
}
|
||||
|
||||
export async function updateWatchHistory(videoId: string, progress: number) {
|
||||
await sodium.ready;
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return;
|
||||
|
||||
const videoHash = await getSecureHash(videoId, rawKey);
|
||||
|
||||
await fetch(`/api/user/history/${videoHash}`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
watched: new Date(),
|
||||
progress
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveWatchHistory(video: VideoPlay, progress: number = 0) {
|
||||
await sodium.ready;
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return;
|
||||
|
||||
const videoHash = await getSecureHash(video.videoId, rawKey);
|
||||
|
||||
const title = await encryptWithMasterKey(video.title);
|
||||
const author = await encryptWithMasterKey(video.author);
|
||||
const thumbnail = await encryptWithMasterKey(getBestThumbnail(video.videoThumbnails));
|
||||
const duration = await encryptWithMasterKey(video.lengthSeconds.toString());
|
||||
|
||||
await fetch('/api/user/history', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
id: videoHash,
|
||||
watched: new Date(),
|
||||
progress: progress,
|
||||
title: {
|
||||
cipher: title?.cipher,
|
||||
nonce: title?.nonce
|
||||
},
|
||||
author: {
|
||||
cipher: author?.cipher,
|
||||
nonce: author?.nonce
|
||||
},
|
||||
thumbnail: {
|
||||
cipher: thumbnail?.cipher,
|
||||
nonce: thumbnail?.nonce
|
||||
},
|
||||
duration: {
|
||||
cipher: duration?.cipher,
|
||||
nonce: duration?.nonce
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async function encryptWithMasterKey(
|
||||
text: string
|
||||
): Promise<{ nonce: string; cipher: string } | undefined> {
|
||||
await sodium.ready;
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return;
|
||||
|
||||
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
||||
const cipher = sodium.crypto_secretbox_easy(new TextEncoder().encode(text), nonce, rawKey);
|
||||
|
||||
return {
|
||||
nonce: sodium.to_base64(nonce),
|
||||
cipher: sodium.to_base64(cipher)
|
||||
};
|
||||
}
|
||||
|
||||
async function decryptWithMasterKey(nonce: string, cipher: string): Promise<string | undefined> {
|
||||
await sodium.ready;
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return;
|
||||
|
||||
return new TextDecoder().decode(
|
||||
sodium.crypto_secretbox_open_easy(sodium.from_base64(cipher), sodium.from_base64(nonce), rawKey)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { rawMasterKeyStore } from '$lib/store';
|
||||
import sodium from 'libsodium-wrappers-sumo';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export async function getSecureHash(toHash: string, rawKey: Uint8Array): Promise<string> {
|
||||
await sodium.ready;
|
||||
return sodium.to_base64(
|
||||
sodium.crypto_generichash(sodium.crypto_generichash_BYTES, toHash, rawKey)
|
||||
);
|
||||
}
|
||||
|
||||
export async function getRawKey(): Promise<Uint8Array | undefined> {
|
||||
const rawMasterKey = get(rawMasterKeyStore);
|
||||
if (!rawMasterKey) return;
|
||||
|
||||
await sodium.ready;
|
||||
|
||||
return sodium.from_base64(rawMasterKey);
|
||||
}
|
||||
|
||||
export async function encryptWithMasterKey(
|
||||
text: string
|
||||
): Promise<{ nonce: string; cipher: string } | undefined> {
|
||||
await sodium.ready;
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return;
|
||||
|
||||
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
||||
const cipher = sodium.crypto_secretbox_easy(new TextEncoder().encode(text), nonce, rawKey);
|
||||
|
||||
return {
|
||||
nonce: sodium.to_base64(nonce),
|
||||
cipher: sodium.to_base64(cipher)
|
||||
};
|
||||
}
|
||||
|
||||
export async function decryptWithMasterKey(
|
||||
nonce: string,
|
||||
cipher: string
|
||||
): Promise<string | undefined> {
|
||||
await sodium.ready;
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return;
|
||||
|
||||
return new TextDecoder().decode(
|
||||
sodium.crypto_secretbox_open_easy(sodium.from_base64(cipher), sodium.from_base64(nonce), rawKey)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import sodium from 'libsodium-wrappers-sumo';
|
||||
import { encryptWithMasterKey, getRawKey, getSecureHash } from './encryption';
|
||||
import type { VideoPlay } from '../model';
|
||||
import { getBestThumbnail } from '$lib/images';
|
||||
|
||||
export async function updateWatchHistory(videoId: string, progress: number) {
|
||||
await sodium.ready;
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return;
|
||||
|
||||
const videoHash = await getSecureHash(videoId, rawKey);
|
||||
|
||||
await fetch(`/api/user/history/${videoHash}`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
watched: new Date(),
|
||||
progress
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveWatchHistory(video: VideoPlay, progress: number = 0) {
|
||||
await sodium.ready;
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return;
|
||||
|
||||
const videoHash = await getSecureHash(video.videoId, rawKey);
|
||||
|
||||
const title = await encryptWithMasterKey(video.title);
|
||||
const author = await encryptWithMasterKey(video.author);
|
||||
const thumbnail = await encryptWithMasterKey(getBestThumbnail(video.videoThumbnails));
|
||||
const duration = await encryptWithMasterKey(video.lengthSeconds.toString());
|
||||
|
||||
await fetch('/api/user/history', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
id: videoHash,
|
||||
watched: new Date(),
|
||||
progress: progress,
|
||||
title: {
|
||||
cipher: title?.cipher,
|
||||
nonce: title?.nonce
|
||||
},
|
||||
author: {
|
||||
cipher: author?.cipher,
|
||||
nonce: author?.nonce
|
||||
},
|
||||
thumbnail: {
|
||||
cipher: thumbnail?.cipher,
|
||||
nonce: thumbnail?.nonce
|
||||
},
|
||||
duration: {
|
||||
cipher: duration?.cipher,
|
||||
nonce: duration?.nonce
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import sodium from 'libsodium-wrappers-sumo';
|
||||
import { rawMasterKeyStore } from '$lib/store';
|
||||
|
||||
export type DerivePassword = (rawPassword: string, passwordSalt: Uint8Array) => Promise<Uint8Array>;
|
||||
|
||||
export async function createUserBackend(
|
||||
username: string,
|
||||
rawPassword: string,
|
||||
captchaPayload: string,
|
||||
derivePassword: DerivePassword
|
||||
): Promise<boolean> {
|
||||
await sodium.ready;
|
||||
|
||||
const passwordSalt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
|
||||
|
||||
const loginHash = await derivePassword(rawPassword, passwordSalt);
|
||||
|
||||
const decryptionKeySalt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
|
||||
const rawDecryptionKey = sodium.crypto_pwhash(
|
||||
32,
|
||||
rawPassword,
|
||||
decryptionKeySalt,
|
||||
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_ALG_DEFAULT
|
||||
);
|
||||
|
||||
const rawDecryptionMasterKey = sodium.randombytes_buf(sodium.crypto_secretbox_KEYBYTES);
|
||||
const decryptionMasterKeyNonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
||||
|
||||
const masterKeyCipher = sodium.crypto_secretbox_easy(
|
||||
rawDecryptionMasterKey,
|
||||
decryptionMasterKeyNonce,
|
||||
rawDecryptionKey
|
||||
);
|
||||
|
||||
const userCreateResp = await fetch('/api/user/create', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: {
|
||||
hash: sodium.to_base64(loginHash),
|
||||
salt: sodium.to_base64(passwordSalt)
|
||||
},
|
||||
decryptionKeySalt: sodium.to_base64(decryptionKeySalt),
|
||||
masterKey: {
|
||||
cipher: sodium.to_base64(masterKeyCipher),
|
||||
nonce: sodium.to_base64(decryptionMasterKeyNonce)
|
||||
},
|
||||
captchaPayload
|
||||
}),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!userCreateResp.ok) return false;
|
||||
|
||||
rawMasterKeyStore.set(sodium.to_base64(rawDecryptionMasterKey));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function loginUserBackend(
|
||||
username: string,
|
||||
rawPassword: string,
|
||||
captchaPayload: string,
|
||||
derivePassword: DerivePassword
|
||||
): Promise<boolean> {
|
||||
await sodium.ready;
|
||||
|
||||
const passwordSaltsResp = await fetch(`/api/user/${username}/public`);
|
||||
if (!passwordSaltsResp.ok) return false;
|
||||
|
||||
const passwordSalts = await passwordSaltsResp.json();
|
||||
|
||||
const loginHash = await derivePassword(
|
||||
rawPassword,
|
||||
sodium.from_base64(passwordSalts.passwordSalt)
|
||||
);
|
||||
|
||||
const loginResp = await fetch('/api/user/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
passwordHash: sodium.to_base64(loginHash),
|
||||
captchaPayload
|
||||
}),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!loginResp.ok) return false;
|
||||
|
||||
const loginJson = await loginResp.json();
|
||||
|
||||
const rawDecryptionKey = sodium.crypto_pwhash(
|
||||
sodium.crypto_secretbox_KEYBYTES,
|
||||
rawPassword,
|
||||
sodium.from_base64(passwordSalts.decryptionKeySalt),
|
||||
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_ALG_DEFAULT
|
||||
);
|
||||
|
||||
const rawDecryptionMasterKey = sodium.crypto_secretbox_open_easy(
|
||||
sodium.from_base64(loginJson.masterKeyCipher),
|
||||
sodium.from_base64(loginJson.masterKeyNonce),
|
||||
rawDecryptionKey
|
||||
);
|
||||
|
||||
rawMasterKeyStore.set(sodium.to_base64(rawDecryptionMasterKey));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { decryptWithMasterKey, encryptWithMasterKey } from './encryption';
|
||||
|
||||
export async function addOrUpdateKeyValue(key: string, value: string) {
|
||||
const valueEncrypted = await encryptWithMasterKey(value);
|
||||
|
||||
await fetch(`/api/user/keyValue/${key}`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
valueCipher: valueEncrypted?.cipher,
|
||||
valueNonce: valueEncrypted?.nonce
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteKeyValue(key: string) {
|
||||
await fetch(`/api/user/keyValue/${key}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
}
|
||||
|
||||
export type KeyValue = boolean | number | string[] | string | object | undefined;
|
||||
|
||||
export async function getKeyValue(key: string): Promise<KeyValue | null> {
|
||||
const resp = await fetch(`/api/user/keyValue/${key}`, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!resp.ok) return null;
|
||||
|
||||
const respJson = await resp.json();
|
||||
return (await decryptWithMasterKey(respJson.valueNonce, respJson.valueCipher)) ?? null;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { parseChannelRSS } from '$lib/api/youtubejs/subscriptions';
|
||||
import { getChannelYTjs } from '$lib/api/youtubejs/channel';
|
||||
import type { ChannelSubscriptions } from '$lib/dexie';
|
||||
import { decryptWithMasterKey, encryptWithMasterKey, getRawKey, getSecureHash } from './encryption';
|
||||
|
||||
export async function getSubscriptionsBackend(): Promise<ChannelSubscriptions[]> {
|
||||
const resp = await fetch(`/api/user/subscriptions`, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!resp.ok) return [];
|
||||
|
||||
const subscriptions: ChannelSubscriptions[] = [];
|
||||
|
||||
const respJson = await resp.json();
|
||||
|
||||
for (const sub of respJson.subscriptions) {
|
||||
subscriptions.push({
|
||||
channelName: (await decryptWithMasterKey(sub.channelNameNonce, sub.channelNameCipher)) ?? '',
|
||||
channelId: (await decryptWithMasterKey(sub.channelIdNonce, sub.channelIdCipher)) ?? '',
|
||||
lastRSSFetch: new Date(sub.lastRSSFetch)
|
||||
});
|
||||
}
|
||||
|
||||
return subscriptions;
|
||||
}
|
||||
|
||||
export async function updateRSSLastUpdated(authorId: string) {
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return false;
|
||||
|
||||
const internalAuthorId = await getSecureHash(authorId, rawKey);
|
||||
|
||||
await fetch(`/api/user/subscriptions/${internalAuthorId}`, {
|
||||
method: 'PATCH',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
}
|
||||
|
||||
export async function amSubscribedBackend(authorId: string): Promise<boolean> {
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return false;
|
||||
|
||||
const internalAuthorId = await getSecureHash(authorId, rawKey);
|
||||
|
||||
const resp = await fetch(`/api/user/subscriptions/${internalAuthorId}`, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
if (!resp.ok) return false;
|
||||
|
||||
const respJson = await resp.json();
|
||||
|
||||
return respJson.amSubscribed;
|
||||
}
|
||||
|
||||
export async function deleteUnsubscribeBackend(authorId: string) {
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return false;
|
||||
|
||||
const internalAuthorId = await getSecureHash(authorId, rawKey);
|
||||
|
||||
await fetch(`/api/user/subscriptions/${internalAuthorId}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
}
|
||||
|
||||
export async function postSubscribeBackend(
|
||||
authorId: string,
|
||||
authorName: string | undefined = undefined
|
||||
) {
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return;
|
||||
|
||||
const internalAuthorId = await getSecureHash(authorId, rawKey);
|
||||
|
||||
if (!authorName) {
|
||||
const channel = await getChannelYTjs(authorId);
|
||||
authorName = channel.author;
|
||||
}
|
||||
|
||||
const channelId = await encryptWithMasterKey(authorId);
|
||||
const channelName = await encryptWithMasterKey(authorName);
|
||||
|
||||
const resp = await fetch(`/api/user/subscriptions/${internalAuthorId}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
channelIdCipher: channelId?.cipher,
|
||||
channelIdNonce: channelId?.nonce,
|
||||
channelNameCipher: channelName?.cipher,
|
||||
channelNameNonce: channelName?.nonce
|
||||
}),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (resp.ok) parseChannelRSS(authorId);
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import type {
|
||||
ReturnYTDislikes,
|
||||
SearchSuggestion,
|
||||
Subscription,
|
||||
ApiExntendedProgressModel,
|
||||
Video,
|
||||
VideoPlay,
|
||||
SearchOptions,
|
||||
@@ -51,7 +50,7 @@ import {
|
||||
deleteUnsubscribeBackend,
|
||||
getSubscriptionsBackend,
|
||||
postSubscribeBackend
|
||||
} from './backend';
|
||||
} from './backend/subscriptions';
|
||||
import { getUserLocale } from '$lib/i18n';
|
||||
|
||||
export function buildPath(path: string): URL {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
engineMaxConcurrentChannelsStore,
|
||||
rawMasterKeyStore
|
||||
} from '$lib/store';
|
||||
import { getSubscriptionsBackend } from '../backend';
|
||||
import { getSubscriptionsBackend } from '../backend/subscriptions';
|
||||
|
||||
export async function getSubscriptionsYTjs(): Promise<Subscription[]> {
|
||||
const subscriptions: Subscription[] = [];
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import { postSubscribeYTjs } from '$lib/api/youtubejs/subscriptions';
|
||||
import { addToast } from '../Toast.svelte';
|
||||
import { isOwnBackend } from '$lib/shared';
|
||||
import { postSubscribeBackend } from '$lib/api/backend';
|
||||
import { postSubscribeBackend } from '$lib/api/backend/subscriptions';
|
||||
|
||||
const engineFallbacks: EngineFallback[] = [
|
||||
'Channel',
|
||||
|
||||
@@ -5,7 +5,7 @@ import { z } from 'zod';
|
||||
import { persistedStores } from './settings';
|
||||
|
||||
import { isOwnBackend } from '$lib/shared';
|
||||
import { addOrUpdateKeyValue, getKeyValue } from '$lib/api/backend';
|
||||
import { addOrUpdateKeyValue, getKeyValue } from '$lib/api/backend/keyvalue';
|
||||
import { rawMasterKeyStore } from '$lib/store';
|
||||
import { getPublicEnv } from '$lib/misc';
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import { error } from '@sveltejs/kit';
|
||||
import { get } from 'svelte/store';
|
||||
import { _ } from './i18n';
|
||||
import { isOwnBackend } from './shared';
|
||||
import { saveWatchHistory } from './api/backend';
|
||||
import { saveWatchHistory } from './api/backend/history';
|
||||
|
||||
export async function getWatchDetails(videoId: string, url: URL) {
|
||||
const playerStateRetrieved = get(playerState);
|
||||
|
||||
Reference in New Issue
Block a user