diff --git a/materialious/src/hooks.server.ts b/materialious/src/hooks.server.ts index 2d2d2da8..b3a493a5 100644 --- a/materialious/src/hooks.server.ts +++ b/materialious/src/hooks.server.ts @@ -1,5 +1,5 @@ import { isOwnBackend } from '$lib/shared'; -import { sequelize } from '$lib/server/database'; +import { getSequelize } from '$lib/server/database'; import { unsign } from 'cookie-signature'; import { env } from '$env/dynamic/private'; import sodium from 'libsodium-wrappers-sumo'; @@ -17,9 +17,10 @@ export async function handle({ event, resolve }) { return await resolve(event); } + const sequelize = getSequelize(); if (!sequelizeAuthenticated) { - await sequelize.sync(); - await sequelize.authenticate(); + await sequelize.sequelize.sync(); + await sequelize.sequelize.authenticate(); sequelizeAuthenticated = true; } diff --git a/materialious/src/lib/server/captcha.ts b/materialious/src/lib/server/captcha.ts index 481337b1..a8636118 100644 --- a/materialious/src/lib/server/captcha.ts +++ b/materialious/src/lib/server/captcha.ts @@ -1,6 +1,6 @@ import { error } from '@sveltejs/kit'; import { verifySolution } from 'altcha-lib'; -import { CaptchaTable } from './database'; +import { getSequelize } from './database'; export async function verifyCaptcha(payload: string, key: string, maxUses: number = -1) { const passedCaptcha = await verifySolution(payload, key, true); @@ -22,11 +22,14 @@ export async function verifyCaptcha(payload: string, key: string, maxUses: numbe } if (maxUses !== -1) { - if ((await CaptchaTable.count({ where: { signature: captchaSignature } })) >= maxUses) { + if ( + (await getSequelize().CaptchaTable.count({ where: { signature: captchaSignature } })) >= + maxUses + ) { throw error(400, 'Unsupported payload'); } - await CaptchaTable.create({ + await getSequelize().CaptchaTable.create({ signature: captchaSignature, created: new Date() }); diff --git a/materialious/src/lib/server/database.ts b/materialious/src/lib/server/database.ts index 94679ca8..66c3a826 100644 --- a/materialious/src/lib/server/database.ts +++ b/materialious/src/lib/server/database.ts @@ -1,70 +1,132 @@ -import { Sequelize, DataTypes, type Model } from 'sequelize'; +import { Sequelize, DataTypes, Model, type ModelCtor } from 'sequelize'; import { env } from '$env/dynamic/private'; -export const sequelize = new Sequelize( - // Use sqlite::memory: if not provided. - env.DATABASE_CONNECTION_URI ? env.DATABASE_CONNECTION_URI : 'sqlite::memory:' -); +let UserTable: ModelCtor>; +let ChannelSubscriptionTable: ModelCtor>; +let CaptchaTable: ModelCtor>; -export interface UserTableModel extends Model { - id: string; - username: string; - passwordHash: string; - passwordSalt: string; - created: Date; - decryptionKeySalt: string; - masterKeyCipher: string; - masterKeyNonce: string; -} +let sequelizeInstance: Sequelize | null = null; -export const UserTable = sequelize.define( - 'User', - { +// Don't want to make our Sequelize instance always +export function getSequelize(): { + sequelize: Sequelize; + UserTable: ModelCtor>; + ChannelSubscriptionTable: ModelCtor>; + CaptchaTable: ModelCtor>; +} { + if (sequelizeInstance) { + return { + sequelize: sequelizeInstance, + UserTable, + ChannelSubscriptionTable, + CaptchaTable + }; + } + + sequelizeInstance = new Sequelize( + env.DATABASE_CONNECTION_URI ? env.DATABASE_CONNECTION_URI : 'sqlite::memory:' + ); + + console.log('sequelizeInstance'); + + UserTable = sequelizeInstance.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 + } + }, + { + indexes: [ + { + unique: true, + fields: ['id', 'username'] + } + ] + } + ); + + ChannelSubscriptionTable = sequelizeInstance.define('Subscriptions', { id: { - type: DataTypes.UUIDV4, - allowNull: false, - primaryKey: true, - unique: true - }, - username: { type: DataTypes.STRING, allowNull: false, - unique: true + primaryKey: true }, - passwordHash: { + channelIdCipher: { type: DataTypes.STRING, allowNull: false }, - passwordSalt: { + channelIdNonce: { type: DataTypes.STRING, allowNull: false }, + channelNameCipher: { + type: DataTypes.STRING, + allowNull: false + }, + channelNameNonce: { + type: DataTypes.STRING, + allowNull: false + }, + lastRSSFetch: { + type: DataTypes.DATE, + allowNull: false + } + }); + + CaptchaTable = sequelizeInstance.define('Captchas', { + signature: { + type: DataTypes.STRING, + allowNull: false, + primaryKey: true + }, 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'] - } - ] - } -); + }); + + UserTable.hasMany(ChannelSubscriptionTable); + + return { + sequelize: sequelizeInstance, + UserTable, + ChannelSubscriptionTable, + CaptchaTable + }; +} export interface ChannelSubscriptionModel { id: string; // Hashed authId with subscription key on client. @@ -76,44 +138,13 @@ export interface ChannelSubscriptionModel { userId: string; } -export const ChannelSubscriptionTable = sequelize.define('Subscriptions', { - id: { - type: DataTypes.STRING, - allowNull: false, - primaryKey: true - }, - channelIdCipher: { - type: DataTypes.STRING, - allowNull: false - }, - channelIdNonce: { - type: DataTypes.STRING, - allowNull: false - }, - channelNameCipher: { - type: DataTypes.STRING, - allowNull: false - }, - channelNameNonce: { - type: DataTypes.STRING, - allowNull: false - }, - lastRSSFetch: { - type: DataTypes.DATE, - allowNull: false - } -}); - -UserTable.hasMany(ChannelSubscriptionTable); - -export const CaptchaTable = sequelize.define('Captchas', { - signature: { - type: DataTypes.STRING, - allowNull: false, - primaryKey: true - }, - created: { - type: DataTypes.DATE, - allowNull: false - } -}); +export interface UserTableModel extends Model { + id: string; + username: string; + passwordHash: string; + passwordSalt: string; + created: Date; + decryptionKeySalt: string; + masterKeyCipher: string; + masterKeyNonce: string; +} diff --git a/materialious/src/lib/server/user.ts b/materialious/src/lib/server/user.ts index 98c4f294..a42487ad 100644 --- a/materialious/src/lib/server/user.ts +++ b/materialious/src/lib/server/user.ts @@ -1,9 +1,4 @@ -import { - ChannelSubscriptionTable, - UserTable, - type ChannelSubscriptionModel, - type UserTableModel -} from './database'; +import { getSequelize, type ChannelSubscriptionModel, type UserTableModel } from './database'; import { Op } from 'sequelize'; import crypto from 'crypto'; import { error } from '@sveltejs/kit'; @@ -35,11 +30,11 @@ export class User { } async delete() { - await UserTable.destroy(this.userWhere); + await getSequelize().UserTable.destroy(this.userWhere); } async subscriptionRssUpdated(id: string) { - await ChannelSubscriptionTable.update( + await getSequelize().ChannelSubscriptionTable.update( { lastRSSFetch: new Date() }, { where: { @@ -51,14 +46,14 @@ export class User { } async addSubscription(subscription: Omit) { - await ChannelSubscriptionTable.create({ + await getSequelize().ChannelSubscriptionTable.create({ ...subscription, UserId: this.id }); } async removeSubscription(id: string) { - await ChannelSubscriptionTable.destroy({ + await getSequelize().ChannelSubscriptionTable.destroy({ where: { id } @@ -67,7 +62,7 @@ export class User { async amSubscribed(id: string): Promise { return ( - (await ChannelSubscriptionTable.count({ + (await getSequelize().ChannelSubscriptionTable.count({ where: { id } @@ -76,7 +71,7 @@ export class User { } async subscriptions(): Promise { - const subscriptions = await ChannelSubscriptionTable.findAll({ + const subscriptions = await getSequelize().ChannelSubscriptionTable.findAll({ where: { userId: this.data.id } @@ -117,7 +112,7 @@ export async function createUser(user: CreateUser): Promise { let userCreated = false; try { - await UserTable.create(createdUser); + await getSequelize().UserTable.create(createdUser); userCreated = true; } catch { userCreated = false; @@ -131,7 +126,7 @@ export async function createUser(user: CreateUser): Promise { } export async function getUser(identifier: string): Promise { - const user = await UserTable.findOne({ + const user = await getSequelize().UserTable.findOne({ where: { [Op.or]: [{ id: identifier }, { username: identifier }] } @@ -145,7 +140,7 @@ export async function getUser(identifier: string): Promise { } export async function authenticateUser(username: string, passwordHash: string): Promise { - const user = await UserTable.findOne({ + const user = await getSequelize().UserTable.findOne({ where: { username } diff --git a/materialious/src/routes/+layout.ts b/materialious/src/routes/+layout.ts index 974f7ec3..718f305c 100644 --- a/materialious/src/routes/+layout.ts +++ b/materialious/src/routes/+layout.ts @@ -22,6 +22,7 @@ import { isYTBackend } from '$lib/misc.js'; import { isOwnBackend } from '$lib/shared/index.js'; export const ssr = false; +export const prerender = false; export async function load({ url }) { if (browser) { @@ -46,18 +47,6 @@ export async function load({ url }) { } } - if (!get(instanceStore) && !isYTBackend() && !url.pathname.endsWith('/setup')) { - goto(resolve('/setup', {}), { replaceState: true }); - } - - if ( - isOwnBackend()?.requireAuth && - !get(rawMasterKeyStore) && - !url.pathname.endsWith('/internal/login') - ) { - goto(resolve('/internal/login', {}), { replaceState: true }); - } - const resolvedRoot = resolve('/', {}); if (url.pathname.startsWith(resolvedRoot + '@')) { const username = url.pathname.substring(resolvedRoot.length).split('/')[0]; @@ -87,4 +76,14 @@ export async function load({ url }) { } }); } + + const isLoginPage = url.pathname.endsWith('/internal/login'); + + if (!isLoginPage) { + if (isOwnBackend()?.requireAuth && !get(rawMasterKeyStore)) { + goto(resolve('/internal/login', {}), { replaceState: true }); + } else if (!get(instanceStore) && !isYTBackend() && !url.pathname.endsWith('/setup')) { + goto(resolve('/setup', {}), { replaceState: true }); + } + } }