Implemented Materialious subscription storage

This commit is contained in:
WardPearce
2026-02-14 04:08:48 +13:00
parent 4b8864bb57
commit 713ede123d
16 changed files with 447 additions and 218 deletions
+244
View File
@@ -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)
);
}
+31
View File
@@ -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>[] = [];
-139
View File
@@ -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' });
}
+48 -42
View File
@@ -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);
+11
View File
@@ -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
});
}
+16 -4
View File
@@ -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);
+2 -2
View File
@@ -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);
+5 -6
View File
@@ -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);
+2 -2
View File
@@ -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)
});
}