import { knex, Model } from './ormish.js'; import bcrypt from 'bcryptjs'; import {v4 as uuid} from "uuid"; import logging from "./logging.js"; import assert from "assert"; import crypto from "crypto"; const log = logging.create("lib/models.js"); export const RESET_CODE_SIZE = 8; // how big to make the unique email reset code export const UNSUB_CODE_SIZE = 16; // how big to make the unique email unsubscribe code export class User extends Model.from_table('user') { static async auth(username, password) { try { const uname = username.toLowerCase(); const res = await knex('user').where({email: uname}).select(); if(res === undefined) { log.error(res, "Received undefined but knex claims only Array returns."); return undefined; } else if(res.length > 1) { log.error(res, `Multiple accounts under the same email ${uname}`); return undefined; } else if(res.length === 0) { // no user found return undefined; } else { // user found, check password const user = res[0]; const good = bcrypt.compareSync(password, user.password); return good ? user : undefined; } } catch(error) { log.error(error); return undefined; } } /** * Performs all the cleanup and checks needed for a registration. It will * ensure that the password and password_repeat are the same, lowercase the email, * clean out unwanted attrbiutes with User.clean, generate required keys, etc. * * @param {Object} attr - attributes usually from a web form * @return {Object} undefined or the new user on success */ static async register(attr) { let user = undefined; if(attr.password != attr.password_repeat) { return undefined; } else { user = User.clean(attr); } // TODO: force emails to lowercase here rather than in the database? user.email = user.email.toLowerCase(); // validate here? let exists = await User.first({email: user.email}); if(exists) { log.error("User exists", user.email); return undefined; } else { // BUG: accepting unfiltered user input user.password = User.encrypt_password(user.password); user.unsubkey = User.random_hex(UNSUB_CODE_SIZE); let res = await knex('user').insert(user); user.id = res[0]; return res.length == 0 ? undefined : user; } } static random_hex(size) { assert(size, `random_hex size can't be falsy ${ size }`); return crypto.randomBytes(size).toString("hex"); } static encrypt_password(password) { assert(password, `Password cannot be falsy: ${ password }`); let salt = bcrypt.genSaltSync(10); return bcrypt.hashSync(password, salt); } async emails(setting) { // don't change it if it's already set this way if(this.unsubscribe !== setting) { return await User.update({id: this.id}, { unsubscribe: setting, unsubscribed_on: Date.now() }); } else { return 1; } } /* Users have many Payments, but Payment has only one User. */ async payments() { return await this.has_many(Payment, { user_id: this.id }); } } export class Payment extends Model.from_table('payment') { /* Payment has only one User, but User has many Payments. */ get user() { return this.user_id !== undefined ? this.has_one(User, { id: this.user_id }) : undefined; } static fake_payment() { return Payment.insert({ system: 'fake', status: 'complete', internal_id: Payment.gen_internal_id(), sys_primary_id: Payment.gen_internal_id(), sys_secondary_id: Payment.gen_internal_id(), sys_created_on: new Date() }); } static gen_internal_id() { return uuid(); } static async paid(user) { assert(user !== undefined, "Invalid user given to Payment.paid"); assert(user.id !== undefined, "User object given has an user.id === undefined."); // ignore refunded payments const payment = await Payment.first(builder => { builder .whereNotIn("status", ["refunded", "failed", "pending"]) .where({user_id: user.id}) }, ["user_id", "system", "status", "status_reason"]); // it's paid if there's a payment and it is complete const paid = payment !== undefined && payment.status === "complete"; // this now returns paid and also paid is false with more info about why return [paid, payment]; } } export class Media extends Model.from_table("media") { } export class Site extends Model.from_table("site") { static async get(key) { const row = await knex(this.table_name).first().where({key}); return row !== undefined ? JSON.parse(row.value) : undefined; } static async set(key, value) { const json_value = JSON.stringify(value); return await Site.upsert({key, value: json_value}, "key"); } // TODO: figure out how to get sqlite3 to do a return of the value static async increment(key, count) { return await knex(this.table_name).where({key}).increment("value", count); } static async decrement(key, count) { return await knex(this.table_name).where({key}).decrement("value", count); } } export class Livestream extends Model.from_table("livestream") { media() { return Media.first({id: this.media_id}); } static add_viewers(id) { return knex(this.table_name).where({id}).increment('viewer_count', 1); } } export class Product extends Model.from_table('product') { }