Implemented Materialious subscription storage
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
import sodium from 'libsodium-wrappers-sumo';
|
||||
import { rawMasterKeyStore } from '../store';
|
||||
import { get } from 'svelte/store';
|
||||
import { parseChannelRSS } from './youtubejs/subscriptions';
|
||||
import type { Subscription } from './model';
|
||||
import { getChannelYTjs } from './youtubejs/channel';
|
||||
import type { ChannelSubscriptions } from '$lib/dexie';
|
||||
|
||||
async function getInternalAuthorId(authorId: string, rawKey: Uint8Array): Promise<string> {
|
||||
await sodium.ready;
|
||||
return sodium.to_base64(
|
||||
sodium.crypto_generichash(sodium.crypto_generichash_BYTES, authorId, 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 getInternalAuthorId(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 getInternalAuthorId(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 getInternalAuthorId(authorId, rawKey);
|
||||
|
||||
await fetch(`/api/user/subscriptions/${internalAuthorId}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
}
|
||||
|
||||
export async function postSubscribeBackend(authorId: string) {
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return;
|
||||
|
||||
const internalAuthorId = await getInternalAuthorId(authorId, rawKey);
|
||||
|
||||
const channel = await getChannelYTjs(authorId);
|
||||
|
||||
const channelId = await encryptWithMasterKey(authorId);
|
||||
const channelName = await encryptWithMasterKey(channel.author);
|
||||
|
||||
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 async function createUserBackend(username: string, rawPassword: string): Promise<boolean> {
|
||||
await sodium.ready;
|
||||
|
||||
const passwordSalt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
|
||||
const loginHash = sodium.crypto_pwhash(
|
||||
32,
|
||||
rawPassword,
|
||||
passwordSalt,
|
||||
sodium.crypto_pwhash_OPSLIMIT_SENSITIVE,
|
||||
sodium.crypto_pwhash_MEMLIMIT_SENSITIVE,
|
||||
sodium.crypto_pwhash_ALG_DEFAULT
|
||||
);
|
||||
|
||||
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)
|
||||
}
|
||||
}),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!userCreateResp.ok) return false;
|
||||
|
||||
rawMasterKeyStore.set(sodium.to_base64(rawDecryptionMasterKey));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function loginUserBackend(username: string, rawPassword: string): 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 = sodium.crypto_pwhash(
|
||||
32,
|
||||
rawPassword,
|
||||
sodium.from_base64(passwordSalts.passwordSalt),
|
||||
sodium.crypto_pwhash_OPSLIMIT_SENSITIVE,
|
||||
sodium.crypto_pwhash_MEMLIMIT_SENSITIVE,
|
||||
sodium.crypto_pwhash_ALG_DEFAULT
|
||||
);
|
||||
|
||||
const loginResp = await fetch('/api/user/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
passwordHash: sodium.to_base64(loginHash)
|
||||
}),
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
interfaceRegionStore,
|
||||
playerYouTubeJsAlways,
|
||||
playerYouTubeJsFallback,
|
||||
rawMasterKeyStore,
|
||||
returnYTDislikesInstanceStore,
|
||||
synciousInstanceStore
|
||||
} from '../store';
|
||||
@@ -45,6 +46,13 @@ import {
|
||||
postSubscribeYTjs
|
||||
} from './youtubejs/subscriptions';
|
||||
import { getPlaylistYTjs } from './youtubejs/playlist';
|
||||
import { isOwnBackend } from '$lib/shared';
|
||||
import {
|
||||
amSubscribedBackend,
|
||||
deleteUnsubscribeBackend,
|
||||
getSubscriptionsBackend,
|
||||
postSubscribeBackend
|
||||
} from './backend';
|
||||
|
||||
export function buildPath(path: string): URL {
|
||||
return new URL(`${get(instanceStore)}/api/v1/${path}`);
|
||||
@@ -278,6 +286,15 @@ export async function getSubscriptions(
|
||||
bypassYTBackend: boolean = false
|
||||
): Promise<Subscription[]> {
|
||||
if (isYTBackend() && !bypassYTBackend) {
|
||||
if (isOwnBackend()?.internalAuth && get(rawMasterKeyStore)) {
|
||||
return (await getSubscriptionsBackend()).map((sub) => {
|
||||
return {
|
||||
author: sub.channelName,
|
||||
authorId: sub.channelId
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return getSubscriptionsYTjs();
|
||||
}
|
||||
const resp = await fetchErrorHandle(
|
||||
@@ -291,6 +308,10 @@ export async function amSubscribed(
|
||||
fetchOptions: RequestInit = {}
|
||||
): Promise<boolean> {
|
||||
if (isYTBackend()) {
|
||||
if (isOwnBackend()?.internalAuth && get(rawMasterKeyStore)) {
|
||||
return amSubscribedBackend(authorId);
|
||||
}
|
||||
|
||||
return amSubscribedYTjs(authorId);
|
||||
}
|
||||
|
||||
@@ -312,6 +333,10 @@ export async function postSubscribe(
|
||||
bypassYTBackend: boolean = false
|
||||
) {
|
||||
if (isYTBackend() && !bypassYTBackend) {
|
||||
if (isOwnBackend()?.internalAuth && get(rawMasterKeyStore)) {
|
||||
return postSubscribeBackend(authorId);
|
||||
}
|
||||
|
||||
return postSubscribeYTjs(authorId);
|
||||
}
|
||||
|
||||
@@ -326,6 +351,12 @@ export async function postSubscribe(
|
||||
|
||||
export async function deleteUnsubscribe(authorId: string, fetchOptions: RequestInit = {}) {
|
||||
if (isYTBackend()) {
|
||||
// deleteUnsubscribeYTjs still should run
|
||||
// as cleans feeds of that channel.
|
||||
if (isOwnBackend()?.internalAuth && get(rawMasterKeyStore)) {
|
||||
deleteUnsubscribeBackend(authorId);
|
||||
}
|
||||
|
||||
return deleteUnsubscribeYTjs(authorId);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { localDb } from '$lib/dexie';
|
||||
import { clearCaches } from '$lib/misc';
|
||||
import { localDb, type ChannelSubscriptions } from '$lib/dexie';
|
||||
import { cleanNumber } from '$lib/numbers';
|
||||
import { relativeTimestamp } from '$lib/time';
|
||||
import { get } from 'svelte/store';
|
||||
@@ -8,8 +7,10 @@ import { getChannelYTjs } from './channel';
|
||||
import {
|
||||
engineCooldownYTStore,
|
||||
engineCullYTStore,
|
||||
engineMaxConcurrentChannelsStore
|
||||
engineMaxConcurrentChannelsStore,
|
||||
rawMasterKeyStore
|
||||
} from '$lib/store';
|
||||
import { getSubscriptionsBackend, updateRSSLastUpdated } from '../backend';
|
||||
|
||||
export async function getSubscriptionsYTjs(): Promise<Subscription[]> {
|
||||
const subscriptions: Subscription[] = [];
|
||||
@@ -54,7 +55,6 @@ export async function postSubscribeYTjs(
|
||||
export async function deleteUnsubscribeYTjs(authorId: string) {
|
||||
await localDb.channelSubscriptions.where('channelId').equals(authorId).delete();
|
||||
await localDb.subscriptionFeed.where('authorId').equals(authorId).delete();
|
||||
clearCaches();
|
||||
}
|
||||
|
||||
export async function parseChannelRSS(channelId: string): Promise<void> {
|
||||
@@ -132,14 +132,24 @@ export async function parseChannelRSS(channelId: string): Promise<void> {
|
||||
// Continue regardless of error
|
||||
}
|
||||
|
||||
await localDb.channelSubscriptions.where('channelId').equals(channelId).modify({
|
||||
lastRSSFetch: new Date()
|
||||
});
|
||||
if (!get(rawMasterKeyStore)) {
|
||||
await localDb.channelSubscriptions.where('channelId').equals(channelId).modify({
|
||||
lastRSSFetch: new Date()
|
||||
});
|
||||
} else {
|
||||
await updateRSSLastUpdated(authorId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFeedYTjs(maxResults: number, page: number): Promise<Feed> {
|
||||
const channelSubscriptions = await localDb.channelSubscriptions.toArray();
|
||||
let channelSubscriptions: ChannelSubscriptions[];
|
||||
|
||||
if (!get(rawMasterKeyStore)) {
|
||||
channelSubscriptions = await localDb.channelSubscriptions.toArray();
|
||||
} else {
|
||||
channelSubscriptions = await getSubscriptionsBackend();
|
||||
}
|
||||
|
||||
const toUpdatePromises: Promise<void>[] = [];
|
||||
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
import sodium from 'libsodium-wrappers-sumo';
|
||||
import { rawSubscriptionKeyStore } from './store';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export async function postSubscribeBackend(authorId: string) {
|
||||
const rawSubscriptionKey = get(rawSubscriptionKeyStore);
|
||||
|
||||
if (!rawSubscriptionKey) return;
|
||||
|
||||
await sodium.ready;
|
||||
|
||||
const rawKey = sodium.from_base64(rawSubscriptionKeyStore);
|
||||
|
||||
const internalAuthorId = sodium.crypto_generichash(
|
||||
sodium.crypto_generichash_BYTES,
|
||||
authorId,
|
||||
rawKey
|
||||
);
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
const channelIdNonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
||||
const channelIdCipher = sodium.crypto_secretbox_easy(
|
||||
textEncoder.encode(authorId),
|
||||
channelIdNonce,
|
||||
rawKey
|
||||
);
|
||||
|
||||
const channelNameNonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
||||
const channelNameCipher = sodium.crypto_secretbox_easy(
|
||||
textEncoder.encode(authorId),
|
||||
channelNameNonce,
|
||||
rawKey
|
||||
);
|
||||
}
|
||||
|
||||
export async function createUserBackend(username: string, rawPassword: string): Promise<boolean> {
|
||||
await sodium.ready;
|
||||
|
||||
const passwordSalt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
|
||||
const loginHash = sodium.crypto_pwhash(
|
||||
32,
|
||||
rawPassword,
|
||||
passwordSalt,
|
||||
sodium.crypto_pwhash_OPSLIMIT_SENSITIVE,
|
||||
sodium.crypto_pwhash_MEMLIMIT_SENSITIVE,
|
||||
sodium.crypto_pwhash_ALG_DEFAULT
|
||||
);
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (!userCreateResp.ok) return false;
|
||||
|
||||
rawSubscriptionKeyStore.set(sodium.to_base64(rawDecryptionMasterKey));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function loginUserBackend(username: string, rawPassword: string): 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 = sodium.crypto_pwhash(
|
||||
32,
|
||||
rawPassword,
|
||||
sodium.from_base64(passwordSalts.passwordSalt),
|
||||
sodium.crypto_pwhash_OPSLIMIT_SENSITIVE,
|
||||
sodium.crypto_pwhash_MEMLIMIT_SENSITIVE,
|
||||
sodium.crypto_pwhash_ALG_DEFAULT
|
||||
);
|
||||
|
||||
const loginResp = await fetch('/api/user/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
passwordHash: sodium.to_base64(loginHash)
|
||||
})
|
||||
});
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
rawSubscriptionKeyStore.set(sodium.to_base64(rawDecryptionMasterKey));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -32,7 +32,7 @@
|
||||
interfaceRegionStore,
|
||||
interfaceSearchHistoryEnabled,
|
||||
interfaceSearchSuggestionsStore,
|
||||
rawSubscriptionKeyStore,
|
||||
rawMasterKeyStore,
|
||||
searchHistoryStore,
|
||||
themeColorStore
|
||||
} from '../../store';
|
||||
@@ -92,7 +92,7 @@
|
||||
|
||||
async function setBackend(event: Event) {
|
||||
if (isOwnBackend()?.internalAuth) {
|
||||
rawSubscriptionKeyStore.set(undefined);
|
||||
rawMasterKeyStore.set(undefined);
|
||||
fetch('/api/user/logout', { method: 'DELETE' });
|
||||
}
|
||||
|
||||
|
||||
@@ -17,42 +17,54 @@ export interface UserTableModel extends Model {
|
||||
masterKeyNonce: string;
|
||||
}
|
||||
|
||||
export const UserTable = sequelize.define('User', {
|
||||
id: {
|
||||
type: DataTypes.UUIDV4,
|
||||
allowNull: false,
|
||||
primaryKey: true
|
||||
export const UserTable = sequelize.define(
|
||||
'User',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUIDV4,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
unique: true
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
passwordHash: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
passwordSalt: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
created: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
decryptionKeySalt: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
masterKeyCipher: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
masterKeyNonce: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
}
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
passwordHash: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
passwordSalt: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
created: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
decryptionKeySalt: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
masterKeyCipher: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
masterKeyNonce: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
{
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['id', 'username']
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
export interface ChannelSubscriptionModel {
|
||||
id: string; // Hashed authId with subscription key on client.
|
||||
@@ -89,13 +101,7 @@ export const ChannelSubscriptionTable = sequelize.define('Subscriptions', {
|
||||
lastRSSFetch: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.UUIDV4,
|
||||
references: {
|
||||
model: 'User',
|
||||
key: 'id'
|
||||
},
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
|
||||
UserTable.hasMany(ChannelSubscriptionTable);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { Cookies } from '@sveltejs/kit';
|
||||
import { sign } from 'cookie-signature';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
export function setAuthCookie(id: string, cookies: Cookies) {
|
||||
cookies.set('userid', sign(id, env.COOKIE_SECRET), {
|
||||
httpOnly: true,
|
||||
path: '/api/user',
|
||||
maxAge: 60 * 60 * 24 * 60 // 60 days
|
||||
});
|
||||
}
|
||||
@@ -38,10 +38,22 @@ export class User {
|
||||
await UserTable.destroy(this.userWhere);
|
||||
}
|
||||
|
||||
async subscriptionRssUpdated(id: string) {
|
||||
await ChannelSubscriptionTable.update(
|
||||
{ lastRSSFetch: new Date() },
|
||||
{
|
||||
where: {
|
||||
id: id,
|
||||
UserId: this.id
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async addSubscription(subscription: Omit<ChannelSubscriptionModel, 'userId'>) {
|
||||
await ChannelSubscriptionTable.create({
|
||||
...subscription,
|
||||
userId: this.id
|
||||
UserId: this.id
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,7 +76,7 @@ export class User {
|
||||
}
|
||||
|
||||
async subscriptions(): Promise<ChannelSubscriptionModel[]> {
|
||||
const subscriptions = await UserTable.findAll({
|
||||
const subscriptions = await ChannelSubscriptionTable.findAll({
|
||||
where: {
|
||||
userId: this.data.id
|
||||
}
|
||||
@@ -90,7 +102,7 @@ export type CreateUser = {
|
||||
};
|
||||
|
||||
export async function createUser(user: CreateUser): Promise<User> {
|
||||
const id = crypto.randomUUID();
|
||||
const id = crypto.randomUUID().toString();
|
||||
|
||||
const createdUser = {
|
||||
id,
|
||||
@@ -155,7 +167,7 @@ export async function authenticateUser(username: string, passwordHash: string):
|
||||
textEncoder.encode(userModel.passwordHash)
|
||||
)
|
||||
) {
|
||||
return new User(userModel as UserTableModel);
|
||||
return new User(userModel);
|
||||
}
|
||||
|
||||
throw error(404);
|
||||
|
||||
@@ -333,10 +333,10 @@ export const engineFallbacksStore: Writable<EngineFallback[]> = persist(
|
||||
'engineFallbacks'
|
||||
);
|
||||
|
||||
export const rawSubscriptionKeyStore: Writable<string | undefined> = persist(
|
||||
export const rawMasterKeyStore: Writable<string | undefined> = persist(
|
||||
writable(),
|
||||
createStorage(),
|
||||
'rawSubscriptionKey'
|
||||
'rawMasterKey'
|
||||
);
|
||||
|
||||
export const syncPartyPeerStore: Writable<Peer | null> = writable(null);
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
isAndroidTvStore,
|
||||
playerState,
|
||||
playertheatreModeIsActive,
|
||||
rawSubscriptionKeyStore,
|
||||
rawMasterKeyStore,
|
||||
syncPartyPeerStore,
|
||||
themeColorStore
|
||||
} from '$lib/store';
|
||||
@@ -40,7 +40,7 @@
|
||||
import { _ } from '$lib/i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { pwaInfo } from 'virtual:pwa-info';
|
||||
import { isYTBackend, clearCaches, truncate } from '$lib/misc';
|
||||
import { isYTBackend, truncate } from '$lib/misc';
|
||||
import Author from '$lib/components/Author.svelte';
|
||||
import Toast from '$lib/components/Toast.svelte';
|
||||
import { isOwnBackend } from '$lib/shared';
|
||||
@@ -189,12 +189,11 @@
|
||||
|
||||
function logout() {
|
||||
if (isOwnBackend()?.internalAuth && isYTBackend()) {
|
||||
rawSubscriptionKeyStore.set(undefined);
|
||||
rawMasterKeyStore.set(undefined);
|
||||
fetch('/api/user/logout', { method: 'DELETE' });
|
||||
}
|
||||
|
||||
authStore.set(null);
|
||||
clearCaches();
|
||||
goto(resolve('/', {}));
|
||||
}
|
||||
|
||||
@@ -273,7 +272,7 @@
|
||||
class:hide={$playertheatreModeIsActive}
|
||||
>
|
||||
<header role="presentation" style="cursor: pointer;" tabindex="-1" class="small-padding">
|
||||
<a href={resolve($interfaceDefaultPage, {})}>
|
||||
<a href={resolve($interfaceDefaultPage, {})} data-sveltekit-preload-data="off">
|
||||
<Logo />
|
||||
</a>
|
||||
</header>
|
||||
@@ -382,7 +381,7 @@
|
||||
</button>
|
||||
|
||||
{#if showLogin}
|
||||
{#if !isLoggedIn && !$rawSubscriptionKeyStore}
|
||||
{#if !isLoggedIn && !$rawMasterKeyStore}
|
||||
<button onclick={login} class="circle large transparent">
|
||||
<i>login</i>
|
||||
<div class="tooltip bottom">{$_('layout.login')}</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { createUserBackend, loginUserBackend } from '$lib/backend';
|
||||
import { createUserBackend, loginUserBackend } from '$lib/api/backend';
|
||||
import { _ } from '$lib/i18n';
|
||||
|
||||
let needToRegister = $state(false);
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
instanceStore,
|
||||
interfaceDefaultPage,
|
||||
isAndroidTvStore,
|
||||
rawSubscriptionKeyStore
|
||||
rawMasterKeyStore
|
||||
} from '$lib/store';
|
||||
import { get, type Writable } from 'svelte/store';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
@@ -36,7 +36,7 @@ export async function load({ url }) {
|
||||
invidiousInstance: instanceStore,
|
||||
authToken: authStore,
|
||||
backendInUse: backendInUseStore,
|
||||
rawSubscriptionKey: rawSubscriptionKeyStore
|
||||
rawMasterKey: rawMasterKeyStore
|
||||
};
|
||||
|
||||
for (const [key, store] of Object.entries(preferenceKey)) {
|
||||
|
||||
@@ -2,9 +2,10 @@ import { isOwnBackend } from '$lib/shared';
|
||||
import { createUser } from '$lib/server/user.js';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import z from 'zod';
|
||||
import { setAuthCookie } from '$lib/server/misc.js';
|
||||
|
||||
const zUserCreate = z.object({
|
||||
username: z.string().min(3).max(32),
|
||||
username: z.string().min(3).max(18),
|
||||
password: z.object({
|
||||
hash: z.string().max(255),
|
||||
salt: z.string().max(255)
|
||||
@@ -16,16 +17,16 @@ const zUserCreate = z.object({
|
||||
})
|
||||
});
|
||||
|
||||
export async function POST({ request }) {
|
||||
export async function POST({ request, cookies }) {
|
||||
if (!isOwnBackend()?.internalAuth || !isOwnBackend()?.registrationAllowed) {
|
||||
return new Response('', { status: 500 });
|
||||
throw error(500);
|
||||
}
|
||||
|
||||
const userToCreate = zUserCreate.safeParse(await request.json());
|
||||
|
||||
if (!userToCreate.success) throw error(400);
|
||||
|
||||
await createUser({
|
||||
const createdUser = await createUser({
|
||||
username: userToCreate.data.username,
|
||||
password: {
|
||||
hash: userToCreate.data.password.hash,
|
||||
@@ -38,5 +39,7 @@ export async function POST({ request }) {
|
||||
}
|
||||
});
|
||||
|
||||
setAuthCookie(createdUser.id, cookies);
|
||||
|
||||
return new Response('');
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { authenticateUser } from '$lib/server/user';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import z from 'zod';
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { sign } from 'cookie-signature';
|
||||
import { isOwnBackend } from '$lib/shared';
|
||||
import { setAuthCookie } from '$lib/server/misc';
|
||||
|
||||
const zUserLogin = z.object({
|
||||
username: z.string(),
|
||||
@@ -12,7 +11,7 @@ const zUserLogin = z.object({
|
||||
|
||||
export async function POST({ request, cookies }) {
|
||||
if (!isOwnBackend()?.internalAuth) {
|
||||
return new Response('', { status: 500 });
|
||||
throw error(500);
|
||||
}
|
||||
|
||||
const userLogin = zUserLogin.safeParse(await request.json());
|
||||
@@ -21,11 +20,7 @@ export async function POST({ request, cookies }) {
|
||||
|
||||
const userModel = await authenticateUser(userLogin.data.username, userLogin.data.passwordHash);
|
||||
|
||||
cookies.set('userid', sign(userModel.id, env.COOKIE_SECRET), {
|
||||
httpOnly: true,
|
||||
path: '/api/user',
|
||||
maxAge: 60 * 60 * 24 * 60 // 60 days
|
||||
});
|
||||
setAuthCookie(userModel.id, cookies);
|
||||
|
||||
return json({
|
||||
masterKeyCipher: userModel.data.masterKeyCipher,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { getUser } from '$lib/server/user';
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
export async function GET({ locals }) {
|
||||
const user = await getUser(locals.userId);
|
||||
|
||||
return json({
|
||||
subscriptions: await user.subscriptions()
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { getUser } from '$lib/server/user';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import z from 'zod';
|
||||
|
||||
const zSubscriptionCreate = z.object({
|
||||
channelIdCipher: z.string().max(255),
|
||||
channelIdNonce: z.string().max(255),
|
||||
channelNameCipher: z.string().max(255),
|
||||
channelNameNonce: z.string().max(255)
|
||||
});
|
||||
|
||||
export async function POST({ locals, request, params }) {
|
||||
const subscription = zSubscriptionCreate.safeParse(await request.json());
|
||||
|
||||
if (!subscription.success) error(400);
|
||||
|
||||
const user = await getUser(locals.userId);
|
||||
await user.addSubscription({
|
||||
...subscription.data,
|
||||
id: params.id,
|
||||
lastRSSFetch: new Date(0)
|
||||
});
|
||||
|
||||
return new Response();
|
||||
}
|
||||
|
||||
export async function PATCH({ locals, params }) {
|
||||
const user = await getUser(locals.userId);
|
||||
await user.subscriptionRssUpdated(params.id);
|
||||
|
||||
return new Response();
|
||||
}
|
||||
|
||||
export async function DELETE({ locals, params }) {
|
||||
const user = await getUser(locals.userId);
|
||||
await user.removeSubscription(params.id);
|
||||
|
||||
return new Response();
|
||||
}
|
||||
|
||||
export async function GET({ locals, params }) {
|
||||
const user = await getUser(locals.userId);
|
||||
|
||||
return json({
|
||||
amSubscribed: await user.amSubscribed(params.id)
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user