mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-11-03 19:07:00 -05:00 
			
		
		
		
	Set up ApiKey model and create Api Key endpoint
This commit is contained in:
		
							parent
							
								
									4f5123e842
								
							
						
					
					
						commit
						d96ed01ce4
					
				@ -47,9 +47,9 @@ class Database {
 | 
				
			|||||||
    return this.models.session
 | 
					    return this.models.session
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /** @type {typeof import('./models/ApiToken')} */
 | 
					  /** @type {typeof import('./models/ApiKey')} */
 | 
				
			||||||
  get apiTokenModel() {
 | 
					  get apiKeyModel() {
 | 
				
			||||||
    return this.models.apiToken
 | 
					    return this.models.apiKey
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /** @type {typeof import('./models/Library')} */
 | 
					  /** @type {typeof import('./models/Library')} */
 | 
				
			||||||
@ -322,7 +322,7 @@ class Database {
 | 
				
			|||||||
  buildModels(force = false) {
 | 
					  buildModels(force = false) {
 | 
				
			||||||
    require('./models/User').init(this.sequelize)
 | 
					    require('./models/User').init(this.sequelize)
 | 
				
			||||||
    require('./models/Session').init(this.sequelize)
 | 
					    require('./models/Session').init(this.sequelize)
 | 
				
			||||||
    require('./models/ApiToken').init(this.sequelize)
 | 
					    require('./models/ApiKey').init(this.sequelize)
 | 
				
			||||||
    require('./models/Library').init(this.sequelize)
 | 
					    require('./models/Library').init(this.sequelize)
 | 
				
			||||||
    require('./models/LibraryFolder').init(this.sequelize)
 | 
					    require('./models/LibraryFolder').init(this.sequelize)
 | 
				
			||||||
    require('./models/Book').init(this.sequelize)
 | 
					    require('./models/Book').init(this.sequelize)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										78
									
								
								server/controllers/ApiKeyController.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								server/controllers/ApiKeyController.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,78 @@
 | 
				
			|||||||
 | 
					const { Request, Response, NextFunction } = require('express')
 | 
				
			||||||
 | 
					const uuidv4 = require('uuid').v4
 | 
				
			||||||
 | 
					const Logger = require('../Logger')
 | 
				
			||||||
 | 
					const Database = require('../Database')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @typedef RequestUserObject
 | 
				
			||||||
 | 
					 * @property {import('../models/User')} user
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @typedef {Request & RequestUserObject} RequestWithUser
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ApiKeyController {
 | 
				
			||||||
 | 
					  constructor() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * POST: /api/api-keys
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {RequestWithUser} req
 | 
				
			||||||
 | 
					   * @param {Response} res
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  async create(req, res) {
 | 
				
			||||||
 | 
					    if (!req.body.name || typeof req.body.name !== 'string') {
 | 
				
			||||||
 | 
					      Logger.warn(`[ApiKeyController] create: Invalid name: ${req.body.name}`)
 | 
				
			||||||
 | 
					      return res.sendStatus(400)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (req.body.expiresIn && (typeof req.body.expiresIn !== 'number' || req.body.expiresIn <= 0)) {
 | 
				
			||||||
 | 
					      Logger.warn(`[ApiKeyController] create: Invalid expiresIn: ${req.body.expiresIn}`)
 | 
				
			||||||
 | 
					      return res.sendStatus(400)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const keyId = uuidv4() // Generate key id ahead of time to use in JWT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const permissions = Database.apiKeyModel.mergePermissionsWithDefault(req.body.permissions)
 | 
				
			||||||
 | 
					    const apiKey = await Database.apiKeyModel.generateApiKey(keyId, req.body.name, req.body.expiresIn)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!apiKey) {
 | 
				
			||||||
 | 
					      Logger.error(`[ApiKeyController] create: Error generating API key`)
 | 
				
			||||||
 | 
					      return res.sendStatus(500)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Calculate expiration time for the api key
 | 
				
			||||||
 | 
					    const expiresAt = req.body.expiresIn ? new Date(Date.now() + req.body.expiresIn * 1000) : null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const apiKeyInstance = await Database.apiKeyModel.create({
 | 
				
			||||||
 | 
					      id: keyId,
 | 
				
			||||||
 | 
					      name: req.body.name,
 | 
				
			||||||
 | 
					      expiresAt,
 | 
				
			||||||
 | 
					      permissions,
 | 
				
			||||||
 | 
					      userId: req.user.id
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return res.json({
 | 
				
			||||||
 | 
					      id: apiKeyInstance.id,
 | 
				
			||||||
 | 
					      name: apiKeyInstance.name,
 | 
				
			||||||
 | 
					      apiKey,
 | 
				
			||||||
 | 
					      expiresAt: apiKeyInstance.expiresAt,
 | 
				
			||||||
 | 
					      permissions: apiKeyInstance.permissions
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {RequestWithUser} req
 | 
				
			||||||
 | 
					   * @param {Response} res
 | 
				
			||||||
 | 
					   * @param {NextFunction} next
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  middleware(req, res, next) {
 | 
				
			||||||
 | 
					    if (!req.user.isAdminOrUp) {
 | 
				
			||||||
 | 
					      Logger.error(`[ApiKeyController] Non-admin user "${req.user.username}" attempting to access api keys`)
 | 
				
			||||||
 | 
					      return res.sendStatus(403)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    next()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = new ApiKeyController()
 | 
				
			||||||
@ -8,11 +8,11 @@
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const migrationVersion = '2.26.0'
 | 
					const migrationVersion = '2.26.0'
 | 
				
			||||||
const migrationName = `${migrationVersion}-create-sessions-table`
 | 
					const migrationName = `${migrationVersion}-create-auth-tables`
 | 
				
			||||||
const loggerPrefix = `[${migrationVersion} migration]`
 | 
					const loggerPrefix = `[${migrationVersion} migration]`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * This upward migration creates a sessions table and apiTokens table.
 | 
					 * This upward migration creates a sessions table and apiKeys table.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * @param {MigrationOptions} options - an object containing the migration context.
 | 
					 * @param {MigrationOptions} options - an object containing the migration context.
 | 
				
			||||||
 * @returns {Promise<void>} - A promise that resolves when the migration is complete.
 | 
					 * @returns {Promise<void>} - A promise that resolves when the migration is complete.
 | 
				
			||||||
@ -68,23 +68,19 @@ async function up({ context: { queryInterface, logger } }) {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Check if table exists
 | 
					  // Check if table exists
 | 
				
			||||||
  if (await queryInterface.tableExists('apiTokens')) {
 | 
					  if (await queryInterface.tableExists('apiKeys')) {
 | 
				
			||||||
    logger.info(`${loggerPrefix} table "apiTokens" already exists`)
 | 
					    logger.info(`${loggerPrefix} table "apiKeys" already exists`)
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    // Create table
 | 
					    // Create table
 | 
				
			||||||
    logger.info(`${loggerPrefix} creating table "apiTokens"`)
 | 
					    logger.info(`${loggerPrefix} creating table "apiKeys"`)
 | 
				
			||||||
    const DataTypes = queryInterface.sequelize.Sequelize.DataTypes
 | 
					    const DataTypes = queryInterface.sequelize.Sequelize.DataTypes
 | 
				
			||||||
    await queryInterface.createTable('apiTokens', {
 | 
					    await queryInterface.createTable('apiKeys', {
 | 
				
			||||||
      id: {
 | 
					      id: {
 | 
				
			||||||
        type: DataTypes.UUID,
 | 
					        type: DataTypes.UUID,
 | 
				
			||||||
        defaultValue: DataTypes.UUIDV4,
 | 
					        defaultValue: DataTypes.UUIDV4,
 | 
				
			||||||
        primaryKey: true
 | 
					        primaryKey: true
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      name: DataTypes.STRING,
 | 
					      name: DataTypes.STRING,
 | 
				
			||||||
      tokenHash: {
 | 
					 | 
				
			||||||
        type: DataTypes.STRING,
 | 
					 | 
				
			||||||
        allowNull: false
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      expiresAt: DataTypes.DATE,
 | 
					      expiresAt: DataTypes.DATE,
 | 
				
			||||||
      lastUsedAt: DataTypes.DATE,
 | 
					      lastUsedAt: DataTypes.DATE,
 | 
				
			||||||
      isActive: {
 | 
					      isActive: {
 | 
				
			||||||
@ -109,18 +105,17 @@ async function up({ context: { queryInterface, logger } }) {
 | 
				
			|||||||
          },
 | 
					          },
 | 
				
			||||||
          key: 'id'
 | 
					          key: 'id'
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        allowNull: false,
 | 
					        onDelete: 'SET NULL'
 | 
				
			||||||
        onDelete: 'CASCADE'
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    logger.info(`${loggerPrefix} created table "apiTokens"`)
 | 
					    logger.info(`${loggerPrefix} created table "apiKeys"`)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
 | 
					  logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * This downward migration script removes the sessions table and apiTokens table.
 | 
					 * This downward migration script removes the sessions table and apiKeys table.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * @param {MigrationOptions} options - an object containing the migration context.
 | 
					 * @param {MigrationOptions} options - an object containing the migration context.
 | 
				
			||||||
 * @returns {Promise<void>} - A promise that resolves when the migration is complete.
 | 
					 * @returns {Promise<void>} - A promise that resolves when the migration is complete.
 | 
				
			||||||
@ -139,12 +134,12 @@ async function down({ context: { queryInterface, logger } }) {
 | 
				
			|||||||
    logger.info(`${loggerPrefix} table "sessions" does not exist`)
 | 
					    logger.info(`${loggerPrefix} table "sessions" does not exist`)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (await queryInterface.tableExists('apiTokens')) {
 | 
					  if (await queryInterface.tableExists('apiKeys')) {
 | 
				
			||||||
    logger.info(`${loggerPrefix} dropping table "apiTokens"`)
 | 
					    logger.info(`${loggerPrefix} dropping table "apiKeys"`)
 | 
				
			||||||
    await queryInterface.dropTable('apiTokens')
 | 
					    await queryInterface.dropTable('apiKeys')
 | 
				
			||||||
    logger.info(`${loggerPrefix} dropped table "apiTokens"`)
 | 
					    logger.info(`${loggerPrefix} dropped table "apiKeys"`)
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    logger.info(`${loggerPrefix} table "apiTokens" does not exist`)
 | 
					    logger.info(`${loggerPrefix} table "apiKeys" does not exist`)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
 | 
					  logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
 | 
				
			||||||
							
								
								
									
										191
									
								
								server/models/ApiKey.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								server/models/ApiKey.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,191 @@
 | 
				
			|||||||
 | 
					const { DataTypes, Model, Op } = require('sequelize')
 | 
				
			||||||
 | 
					const jwt = require('jsonwebtoken')
 | 
				
			||||||
 | 
					const Logger = require('../Logger')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @typedef {Object} ApiKeyPermissions
 | 
				
			||||||
 | 
					 * @property {boolean} download
 | 
				
			||||||
 | 
					 * @property {boolean} update
 | 
				
			||||||
 | 
					 * @property {boolean} delete
 | 
				
			||||||
 | 
					 * @property {boolean} upload
 | 
				
			||||||
 | 
					 * @property {boolean} createEreader
 | 
				
			||||||
 | 
					 * @property {boolean} accessAllLibraries
 | 
				
			||||||
 | 
					 * @property {boolean} accessAllTags
 | 
				
			||||||
 | 
					 * @property {boolean} accessExplicitContent
 | 
				
			||||||
 | 
					 * @property {boolean} selectedTagsNotAccessible
 | 
				
			||||||
 | 
					 * @property {string[]} librariesAccessible
 | 
				
			||||||
 | 
					 * @property {string[]} itemTagsSelected
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ApiKey extends Model {
 | 
				
			||||||
 | 
					  constructor(values, options) {
 | 
				
			||||||
 | 
					    super(values, options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @type {UUIDV4} */
 | 
				
			||||||
 | 
					    this.id
 | 
				
			||||||
 | 
					    /** @type {string} */
 | 
				
			||||||
 | 
					    this.name
 | 
				
			||||||
 | 
					    /** @type {Date} */
 | 
				
			||||||
 | 
					    this.expiresAt
 | 
				
			||||||
 | 
					    /** @type {Date} */
 | 
				
			||||||
 | 
					    this.lastUsedAt
 | 
				
			||||||
 | 
					    /** @type {boolean} */
 | 
				
			||||||
 | 
					    this.isActive
 | 
				
			||||||
 | 
					    /** @type {Object} */
 | 
				
			||||||
 | 
					    this.permissions
 | 
				
			||||||
 | 
					    /** @type {Date} */
 | 
				
			||||||
 | 
					    this.createdAt
 | 
				
			||||||
 | 
					    /** @type {Date} */
 | 
				
			||||||
 | 
					    this.updatedAt
 | 
				
			||||||
 | 
					    /** @type {UUIDV4} */
 | 
				
			||||||
 | 
					    this.userId
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Expanded properties
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @type {import('./User').User} */
 | 
				
			||||||
 | 
					    this.user
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Same properties as User.getDefaultPermissions
 | 
				
			||||||
 | 
					   * @returns {ApiKeyPermissions}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static getDefaultPermissions() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      download: true,
 | 
				
			||||||
 | 
					      update: true,
 | 
				
			||||||
 | 
					      delete: true,
 | 
				
			||||||
 | 
					      upload: true,
 | 
				
			||||||
 | 
					      createEreader: true,
 | 
				
			||||||
 | 
					      accessAllLibraries: true,
 | 
				
			||||||
 | 
					      accessAllTags: true,
 | 
				
			||||||
 | 
					      accessExplicitContent: true,
 | 
				
			||||||
 | 
					      selectedTagsNotAccessible: false, // Inverts itemTagsSelected
 | 
				
			||||||
 | 
					      librariesAccessible: [],
 | 
				
			||||||
 | 
					      itemTagsSelected: []
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Merge permissions from request with default permissions
 | 
				
			||||||
 | 
					   * @param {ApiKeyPermissions} reqPermissions
 | 
				
			||||||
 | 
					   * @returns {ApiKeyPermissions}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static mergePermissionsWithDefault(reqPermissions) {
 | 
				
			||||||
 | 
					    const permissions = this.getDefaultPermissions()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!reqPermissions || typeof reqPermissions !== 'object') {
 | 
				
			||||||
 | 
					      Logger.warn(`[ApiKey] mergePermissionsWithDefault: Invalid permissions: ${reqPermissions}`)
 | 
				
			||||||
 | 
					      return permissions
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const key in reqPermissions) {
 | 
				
			||||||
 | 
					      if (reqPermissions[key] === undefined) {
 | 
				
			||||||
 | 
					        Logger.warn(`[ApiKey] mergePermissionsWithDefault: Invalid permission key: ${key}`)
 | 
				
			||||||
 | 
					        continue
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (key === 'librariesAccessible' || key === 'itemTagsSelected') {
 | 
				
			||||||
 | 
					        if (!Array.isArray(reqPermissions[key]) || reqPermissions[key].some((value) => typeof value !== 'string')) {
 | 
				
			||||||
 | 
					          Logger.warn(`[ApiKey] mergePermissionsWithDefault: Invalid ${key} value: ${reqPermissions[key]}`)
 | 
				
			||||||
 | 
					          continue
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        permissions[key] = reqPermissions[key]
 | 
				
			||||||
 | 
					      } else if (typeof reqPermissions[key] !== 'boolean') {
 | 
				
			||||||
 | 
					        Logger.warn(`[ApiKey] mergePermissionsWithDefault: Invalid permission value for key ${key}. Should be boolean`)
 | 
				
			||||||
 | 
					        continue
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      permissions[key] = reqPermissions[key]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return permissions
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Clean up expired api keys from the database
 | 
				
			||||||
 | 
					   * @returns {Promise<number>} Number of api keys deleted
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static async cleanupExpiredApiKeys() {
 | 
				
			||||||
 | 
					    const deletedCount = await ApiKey.destroy({
 | 
				
			||||||
 | 
					      where: {
 | 
				
			||||||
 | 
					        expiresAt: {
 | 
				
			||||||
 | 
					          [Op.lt]: new Date()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    return deletedCount
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Generate a new api key
 | 
				
			||||||
 | 
					   * @param {string} keyId
 | 
				
			||||||
 | 
					   * @param {string} name
 | 
				
			||||||
 | 
					   * @param {number} [expiresIn] - Seconds until the api key expires or undefined for no expiration
 | 
				
			||||||
 | 
					   * @returns {Promise<string>}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static async generateApiKey(keyId, name, expiresIn) {
 | 
				
			||||||
 | 
					    const options = {}
 | 
				
			||||||
 | 
					    if (expiresIn && !isNaN(expiresIn) && expiresIn > 0) {
 | 
				
			||||||
 | 
					      options.expiresIn = expiresIn
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Promise((resolve) => {
 | 
				
			||||||
 | 
					      jwt.sign(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          keyId,
 | 
				
			||||||
 | 
					          name,
 | 
				
			||||||
 | 
					          type: 'api'
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        global.ServerSettings.tokenSecret,
 | 
				
			||||||
 | 
					        options,
 | 
				
			||||||
 | 
					        (err, token) => {
 | 
				
			||||||
 | 
					          if (err) {
 | 
				
			||||||
 | 
					            Logger.error(`[ApiKey] Error generating API key: ${err}`)
 | 
				
			||||||
 | 
					            resolve(null)
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            resolve(token)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Initialize model
 | 
				
			||||||
 | 
					   * @param {import('../Database').sequelize} sequelize
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static init(sequelize) {
 | 
				
			||||||
 | 
					    super.init(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: {
 | 
				
			||||||
 | 
					          type: DataTypes.UUID,
 | 
				
			||||||
 | 
					          defaultValue: DataTypes.UUIDV4,
 | 
				
			||||||
 | 
					          primaryKey: true
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        name: DataTypes.STRING,
 | 
				
			||||||
 | 
					        expiresAt: DataTypes.DATE,
 | 
				
			||||||
 | 
					        lastUsedAt: DataTypes.DATE,
 | 
				
			||||||
 | 
					        isActive: {
 | 
				
			||||||
 | 
					          type: DataTypes.BOOLEAN,
 | 
				
			||||||
 | 
					          allowNull: false,
 | 
				
			||||||
 | 
					          defaultValue: false
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        permissions: DataTypes.JSON
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        sequelize,
 | 
				
			||||||
 | 
					        modelName: 'apiKey'
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { user } = sequelize.models
 | 
				
			||||||
 | 
					    user.hasMany(ApiKey, {
 | 
				
			||||||
 | 
					      onDelete: 'SET NULL'
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    ApiKey.belongsTo(user)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = ApiKey
 | 
				
			||||||
@ -1,90 +0,0 @@
 | 
				
			|||||||
const { DataTypes, Model, Op } = require('sequelize')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class ApiToken extends Model {
 | 
					 | 
				
			||||||
  constructor(values, options) {
 | 
					 | 
				
			||||||
    super(values, options)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /** @type {UUIDV4} */
 | 
					 | 
				
			||||||
    this.id
 | 
					 | 
				
			||||||
    /** @type {string} */
 | 
					 | 
				
			||||||
    this.name
 | 
					 | 
				
			||||||
    /** @type {string} */
 | 
					 | 
				
			||||||
    this.tokenHash
 | 
					 | 
				
			||||||
    /** @type {Date} */
 | 
					 | 
				
			||||||
    this.expiresAt
 | 
					 | 
				
			||||||
    /** @type {Date} */
 | 
					 | 
				
			||||||
    this.lastUsedAt
 | 
					 | 
				
			||||||
    /** @type {boolean} */
 | 
					 | 
				
			||||||
    this.isActive
 | 
					 | 
				
			||||||
    /** @type {Object} */
 | 
					 | 
				
			||||||
    this.permissions
 | 
					 | 
				
			||||||
    /** @type {Date} */
 | 
					 | 
				
			||||||
    this.createdAt
 | 
					 | 
				
			||||||
    /** @type {UUIDV4} */
 | 
					 | 
				
			||||||
    this.userId
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Expanded properties
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /** @type {import('./User').User} */
 | 
					 | 
				
			||||||
    this.user
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Clean up expired api tokens from the database
 | 
					 | 
				
			||||||
   * @returns {Promise<number>} Number of api tokens deleted
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  static async cleanupExpiredApiTokens() {
 | 
					 | 
				
			||||||
    const deletedCount = await ApiToken.destroy({
 | 
					 | 
				
			||||||
      where: {
 | 
					 | 
				
			||||||
        expiresAt: {
 | 
					 | 
				
			||||||
          [Op.lt]: new Date()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    return deletedCount
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Initialize model
 | 
					 | 
				
			||||||
   * @param {import('../Database').sequelize} sequelize
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  static init(sequelize) {
 | 
					 | 
				
			||||||
    super.init(
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        id: {
 | 
					 | 
				
			||||||
          type: DataTypes.UUID,
 | 
					 | 
				
			||||||
          defaultValue: DataTypes.UUIDV4,
 | 
					 | 
				
			||||||
          primaryKey: true
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        name: DataTypes.STRING,
 | 
					 | 
				
			||||||
        tokenHash: {
 | 
					 | 
				
			||||||
          type: DataTypes.STRING,
 | 
					 | 
				
			||||||
          allowNull: false
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        expiresAt: DataTypes.DATE,
 | 
					 | 
				
			||||||
        lastUsedAt: DataTypes.DATE,
 | 
					 | 
				
			||||||
        isActive: {
 | 
					 | 
				
			||||||
          type: DataTypes.BOOLEAN,
 | 
					 | 
				
			||||||
          allowNull: false,
 | 
					 | 
				
			||||||
          defaultValue: false
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        permissions: DataTypes.JSON
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        sequelize,
 | 
					 | 
				
			||||||
        modelName: 'apiToken'
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const { user } = sequelize.models
 | 
					 | 
				
			||||||
    user.hasMany(ApiToken, {
 | 
					 | 
				
			||||||
      onDelete: 'CASCADE',
 | 
					 | 
				
			||||||
      foreignKey: {
 | 
					 | 
				
			||||||
        allowNull: false
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    ApiToken.belongsTo(user)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
module.exports = ApiToken
 | 
					 | 
				
			||||||
@ -34,6 +34,7 @@ const CustomMetadataProviderController = require('../controllers/CustomMetadataP
 | 
				
			|||||||
const MiscController = require('../controllers/MiscController')
 | 
					const MiscController = require('../controllers/MiscController')
 | 
				
			||||||
const ShareController = require('../controllers/ShareController')
 | 
					const ShareController = require('../controllers/ShareController')
 | 
				
			||||||
const StatsController = require('../controllers/StatsController')
 | 
					const StatsController = require('../controllers/StatsController')
 | 
				
			||||||
 | 
					const ApiKeyController = require('../controllers/ApiKeyController')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ApiRouter {
 | 
					class ApiRouter {
 | 
				
			||||||
  constructor(Server) {
 | 
					  constructor(Server) {
 | 
				
			||||||
@ -325,6 +326,11 @@ class ApiRouter {
 | 
				
			|||||||
    this.router.get('/stats/year/:year', StatsController.middleware.bind(this), StatsController.getAdminStatsForYear.bind(this))
 | 
					    this.router.get('/stats/year/:year', StatsController.middleware.bind(this), StatsController.getAdminStatsForYear.bind(this))
 | 
				
			||||||
    this.router.get('/stats/server', StatsController.middleware.bind(this), StatsController.getServerStats.bind(this))
 | 
					    this.router.get('/stats/server', StatsController.middleware.bind(this), StatsController.getServerStats.bind(this))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //
 | 
				
			||||||
 | 
					    // API Key Routes
 | 
				
			||||||
 | 
					    //
 | 
				
			||||||
 | 
					    this.router.post('/api-keys', ApiKeyController.middleware.bind(this), ApiKeyController.create.bind(this))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    //
 | 
					    //
 | 
				
			||||||
    // Misc Routes
 | 
					    // Misc Routes
 | 
				
			||||||
    //
 | 
					    //
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user