audiobookshelf/server/models/AudibleAccount.js

170 lines
4.3 KiB
JavaScript

const { DataTypes, Model } = require('sequelize')
const crypto = require('crypto')
const Logger = require('../Logger')
const ALGORITHM = 'aes-256-cbc'
function getEncryptionKey() {
const secret = process.env.AUDIBLE_ENCRYPTION_KEY || process.env.TOKEN_SECRET
if (!secret) throw new Error('[AudibleAccount] No encryption key available — set AUDIBLE_ENCRYPTION_KEY or TOKEN_SECRET')
return crypto.scryptSync(secret, 'audible-salt', 32)
}
function encrypt(text) {
if (!text) return null
const iv = crypto.randomBytes(16)
const cipher = crypto.createCipheriv(ALGORITHM, getEncryptionKey(), iv)
const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()])
return iv.toString('hex') + ':' + encrypted.toString('hex')
}
function decrypt(data) {
if (!data) return null
try {
const [ivHex, encHex] = data.split(':')
const decipher = crypto.createDecipheriv(ALGORITHM, getEncryptionKey(), Buffer.from(ivHex, 'hex'))
const decrypted = Buffer.concat([decipher.update(Buffer.from(encHex, 'hex')), decipher.final()])
return decrypted.toString('utf8')
} catch (err) {
Logger.error('[AudibleAccount] Failed to decrypt token', err.message)
return null
}
}
/**
* @typedef ClientAudibleAccount
* @property {string} id
* @property {string} userId
* @property {string} email
* @property {string} region
* @property {Date|null} lastSync
* @property {boolean} isActive
* @property {Date} createdAt
*/
class AudibleAccount extends Model {
constructor(values, options) {
super(values, options)
/** @type {string} */
this.id
/** @type {string} */
this.userId
/** @type {string} */
this.email
/** @type {string} */
this.region
/** @type {string} encrypted access token */
this.encryptedToken
/** @type {string|null} encrypted website cookies JSON */
this.encryptedCookies
/** @type {string|null} library to show the preorders shelf on */
this.libraryId
/** @type {number} position in the shelf list (0 = first) */
this.shelfPosition
/** @type {Date|null} */
this.lastSync
/** @type {boolean} */
this.isActive
/** @type {Date} */
this.createdAt
/** @type {Date} */
this.updatedAt
}
getAccessToken() {
return decrypt(this.encryptedToken)
}
setAccessToken(token) {
this.encryptedToken = encrypt(token)
}
getWebsiteCookies() {
const raw = decrypt(this.encryptedCookies)
if (!raw) return null
try { return JSON.parse(raw) } catch { return null }
}
setWebsiteCookies(cookiesObj) {
this.encryptedCookies = cookiesObj ? encrypt(JSON.stringify(cookiesObj)) : null
}
/**
* @returns {ClientAudibleAccount}
*/
toClientJson() {
return {
id: this.id,
userId: this.userId,
email: this.email,
region: this.region,
libraryId: this.libraryId || null,
shelfPosition: this.shelfPosition || 0,
lastSync: this.lastSync,
isActive: this.isActive,
createdAt: this.createdAt
}
}
/**
* @param {import('../Database').sequelize} sequelize
*/
static init(sequelize) {
super.init(
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
email: {
type: DataTypes.STRING,
allowNull: false
},
region: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'us'
},
encryptedToken: {
type: DataTypes.TEXT,
allowNull: false
},
encryptedCookies: {
type: DataTypes.TEXT,
allowNull: true
},
libraryId: {
type: DataTypes.UUID,
allowNull: true
},
shelfPosition: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
},
lastSync: DataTypes.DATE,
isActive: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true
}
},
{
sequelize,
modelName: 'audibleAccount',
indexes: [
{ fields: ['userId'] },
{ unique: true, fields: ['email', 'userId'] }
]
}
)
const { user } = sequelize.models
user.hasMany(AudibleAccount, { onDelete: 'CASCADE' })
AudibleAccount.belongsTo(user)
}
}
module.exports = AudibleAccount