diff --git a/CHANGELOG.md b/CHANGELOG.md index 6191fa4..e7b983d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - Introduce new administrative channel management endpoints `/admin/v1/realms/{realmId}/channels`. - Introduce new administrative subscription management endpoints `/admin/v1/realms/{realmId}/subscriptions`. - Introduce new administrative endpoint for deleting auth tokens `DELETE /admin/v1/realms/{realmId}/tokens/{tokenId}` +- Introduce limit settings on realm level ([#151]). ### Changed - Add support for upgraded verneMQ broker. This requires the acceptance of their [EULA](https://vernemq.com/end-user-license-agreement/), @@ -20,6 +21,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - Hide secret `auth.token` from all responses, except token creation, to avoid access right escalation. ([#138]) [#138]: https://github.com/realmq/realmq-platform/issues/138 +[#151]: https://github.com/realmq/realmq-platform/issues/151 ## [0.2.0] - 2023-03-09 diff --git a/src/api/admin/v1/mappers/index.js b/src/api/admin/v1/mappers/index.js index 05efe5d..ece75c9 100644 --- a/src/api/admin/v1/mappers/index.js +++ b/src/api/admin/v1/mappers/index.js @@ -3,6 +3,7 @@ const authList = require('./auth-list'); const channel = require('./channel'); const channelList = require('./channel-list'); const realm = require('./realm'); +const realmDetails = require('./realm-details'); const realmList = require('./realm-list'); const subscription = require('./subscription'); const subscriptionList = require('./subscription-list'); @@ -18,6 +19,7 @@ module.exports = { channel, channelList, realm, + realmDetails, realmList, subscription, subscriptionList, diff --git a/src/api/admin/v1/mappers/realm-details.js b/src/api/admin/v1/mappers/realm-details.js new file mode 100644 index 0000000..d400f52 --- /dev/null +++ b/src/api/admin/v1/mappers/realm-details.js @@ -0,0 +1,30 @@ +const mapRealm = require('./realm'); +const mapGenericEntity = require('../../../../lib/mappers/generic-entity'); + +/** + * @class RealmViewModel + * @property {string} id + * @property {string} name + * @property {Date} createdAt + * @property {Date} updatedAt + */ +/** + * Map a realm to detail response view model + * @param {object} args + * @param {RealmModel} args.realm The realm entity + * @param {RealmLimitsModel} args.realmLimits The realm limits entity + * @return {RealmViewModel} The realm view model + */ +module.exports = ({realm, realmLimits}) => ({ + ...mapRealm(realm), + limits: mapGenericEntity({ + entity: realmLimits, + propertyMap: { + maxConnections: 'maxConnections', + sessionMaxMessageRate: 'sessionMaxMessageRate', + sessionMaxConnectionLifetime: 'sessionMaxConnectionLifetime', + sessionMaxMessageSize: 'sessionMaxMessageSize', + } + }), +}); + diff --git a/src/api/admin/v1/mappers/realm.js b/src/api/admin/v1/mappers/realm.js index f165a8d..49073dc 100644 --- a/src/api/admin/v1/mappers/realm.js +++ b/src/api/admin/v1/mappers/realm.js @@ -8,7 +8,7 @@ const mapGenericEntity = require('../../../../lib/mappers/generic-entity'); * @property {Date} updatedAt */ /** - * Map an realm to response view model + * Map a realm to response view model * @param {RealmModel} realm The realm entity * @return {RealmViewModel} The realm view model */ diff --git a/src/api/admin/v1/openapi/definitions.yaml b/src/api/admin/v1/openapi/definitions.yaml index 250e292..7a86d40 100644 --- a/src/api/admin/v1/openapi/definitions.yaml +++ b/src/api/admin/v1/openapi/definitions.yaml @@ -74,13 +74,39 @@ TimestampsTrait: Realm: allOf: - $ref: '#/definitions/BaseEntity' - - properties: + - type: object + properties: name: type: string required: - name - $ref: '#/definitions/TimestampsTrait' +RealmDetails: + allOf: + - $ref: '#/definitions/Realm' + - type: object + properties: + limits: + $ref: '#/definitions/RealmLimits' + +RealmLimits: + type: object + properties: + maxConnections: + description: Maximum number of simultaneously online connections + type: integer + sessionMaxMessageRate: + description: Maximum incoming publish rate per session per second + type: integer + sessionMaxConnectionLifetime: + description: Maximum lifetime of a connection in seconds + type: integer + sessionMaxMessageSize: + description: Maximum size of a message payload in bytes + type: integer + additionalProperties: false + RealmList: allOf: - $ref: '#/definitions/BaseList' diff --git a/src/api/admin/v1/routes/realms.js b/src/api/admin/v1/routes/realms.js index 773e880..b879954 100644 --- a/src/api/admin/v1/routes/realms.js +++ b/src/api/admin/v1/routes/realms.js @@ -16,13 +16,16 @@ module.exports = (tasks, mappers) => ({ }, async post(request, response) { - const {body: {name}} = request; - const {ok, result: realm, error} = await tasks.admin.createRealm({name}); + const {body: {name, limits = {}}} = request; + const {ok, result, error} = await tasks.admin.createRealm({name, limits}); if (!ok) { throw error; } - return response.status(201).json(mappers.realm(realm)); + return response.status(201).json(mappers.realm({ + realm: result.realm, + realmLimits: result.realmLimits + })); }, }); diff --git a/src/api/admin/v1/routes/realms.yaml b/src/api/admin/v1/routes/realms.yaml index 1f9c0fc..fa75570 100644 --- a/src/api/admin/v1/routes/realms.yaml +++ b/src/api/admin/v1/routes/realms.yaml @@ -40,11 +40,13 @@ name: description: The name of the real, eg. project/org name type: string + limits: + $ref: '#/definitions/RealmLimits' responses: 201: description: Realm was successfully created. schema: - $ref: '#/definitions/Realm' + $ref: '#/definitions/RealmDetails' 400: description: Request validation failed. 401: diff --git a/src/bootstrap/repositories.js b/src/bootstrap/repositories.js index 8c9c71c..3d75688 100644 --- a/src/bootstrap/repositories.js +++ b/src/bootstrap/repositories.js @@ -12,6 +12,7 @@ module.exports = async ({db}) => { channelCollection, messageCollection, realmCollection, + realmLimitsCollection, realtimeConnectionCollection, subscriptionCollection, userCollection, @@ -20,6 +21,7 @@ module.exports = async ({db}) => { db.collection('channels'), db.collection('messages'), db.collection('realms'), + db.collection('realm-limits'), db.collection('realtime-connections'), db.collection('subscriptions'), db.collection('users'), @@ -29,6 +31,7 @@ module.exports = async ({db}) => { channelCollection, messageCollection, realmCollection, + realmLimitsCollection, realtimeConnectionCollection, subscriptionCollection, userCollection, diff --git a/src/bootstrap/tasks.js b/src/bootstrap/tasks.js index f386c2b..502dceb 100644 --- a/src/bootstrap/tasks.js +++ b/src/bootstrap/tasks.js @@ -26,6 +26,7 @@ module.exports = ({ channel: channelRepository, message: messageRepository, realm: realmRepository, + realmLimits: realmLimitsRepository, realtimeConnection: realtimeConnectionRepository, subscription: subscriptionRepository, user: userRepository, @@ -44,6 +45,7 @@ module.exports = ({ authRepository, channelRepository, realmRepository, + realmLimitsRepository, subscriptionRepository, userRepository, sendSubscriptionSyncMessage, @@ -54,6 +56,7 @@ module.exports = ({ authRepository, channelRepository, subscriptionRepository, + realmLimitsRepository, realtimeConnectionRepository, messageRepository, userRepository, diff --git a/src/index.js b/src/index.js index 2c775a6..8623889 100644 --- a/src/index.js +++ b/src/index.js @@ -34,3 +34,4 @@ const createLogger = require('./lib/logger'); process.exit(1); } })(); + diff --git a/src/models/realm-limits.js b/src/models/realm-limits.js new file mode 100644 index 0000000..f8ce8f9 --- /dev/null +++ b/src/models/realm-limits.js @@ -0,0 +1,36 @@ +const stripUndefined = require('../lib/strip-undefined'); + +/** + * @typedef {Object} RealmLimitsModel Specialized set of realm settings focused on connections/sessions + * @param {string} [id] + * @param {string} realmId + * @param {number} maxConnections Max number of simultaneously online connections + * @param {number} sessionMaxMessageRate Max incoming publish rate per session per second + * @param {number} sessionMaxConnectionLifetime Max lifetime of a connection in seconds + * @param {number} sessionMaxMessageSize Max message payload size in bytes + * @param {Date} [createdAt] + * @param {Date} [updatedAt] + */ + +/** + * @return {RealmLimitsModel} The generalized realm model + */ +module.exports = ({ + id, + realmId, + maxConnections, + sessionMaxMessageRate, + sessionMaxConnectionLifetime, + sessionMaxMessageSize, + createdAt, + updatedAt, +}) => stripUndefined({ + id, + realmId, + maxConnections, + sessionMaxMessageRate, + sessionMaxConnectionLifetime, + sessionMaxMessageSize, + createdAt, + updatedAt, +}); diff --git a/src/repositories/index.js b/src/repositories/index.js index 2d9b6dd..b18d415 100644 --- a/src/repositories/index.js +++ b/src/repositories/index.js @@ -2,6 +2,7 @@ const createAuthRepository = require('./auth'); const createChannelRepository = require('./channel'); const createMessageRepository = require('./message'); const createRealmRepository = require('./realm'); +const createRealmLimitsRepository = require('./realm-limits'); const createRealtimeConnectionRepository = require('./realtime-connection'); const createSubscriptionRepository = require('./subscription'); const createUserRepository = require('./user'); @@ -13,14 +14,20 @@ const createUserRepository = require('./user'); * @param {Collection} channelCollection Dependency * @param {Collection} messageCollection Dependency * @param {Collection} realmCollection Dependency + * @param {Collection} realmLimitsCollection Dependency * @param {Collection} realtimeConnectionCollection Dependency * @param {Collection} subscriptionCollection Dependency * @param {Collection} userCollection Dependency * @return {Repositories} The repositories */ module.exports = ({ - authCollection, channelCollection, messageCollection, - realmCollection, subscriptionCollection, userCollection, + authCollection, + channelCollection, + messageCollection, + realmCollection, + realmLimitsCollection, + subscriptionCollection, + userCollection, realtimeConnectionCollection, }) => /** @@ -29,6 +36,7 @@ module.exports = ({ * @property {ChannelRepository} channel * @property {MessageRepository} message * @property {RealmRepository} realm + * @property {RealmLimitsRepository} realmLimits * @property {RealtimeConnectionRepository} realtimeConnection * @property {SubscriptionRepository} subscription * @property {UserRepository} user @@ -38,6 +46,7 @@ module.exports = ({ channel: createChannelRepository({collection: channelCollection}), message: createMessageRepository({collection: messageCollection}), realm: createRealmRepository({collection: realmCollection}), + realmLimits: createRealmLimitsRepository({collection: realmLimitsCollection}), realtimeConnection: createRealtimeConnectionRepository({collection: realtimeConnectionCollection}), subscription: createSubscriptionRepository({collection: subscriptionCollection}), user: createUserRepository({collection: userCollection}), diff --git a/src/repositories/realm-limits.js b/src/repositories/realm-limits.js new file mode 100644 index 0000000..37c6430 --- /dev/null +++ b/src/repositories/realm-limits.js @@ -0,0 +1,22 @@ +const createRealmLimitsModel = require('../models/realm-limits'); +const createMongoRepository = require('./lib/mongo'); + +/** + * Create realm repository. + * + * @param {Collection} collection Mongodb collection + * @param {function} createModel Model factory + * @return {RealmLimitsRepository} The created realm repository + */ +module.exports = ({collection, createModel = createRealmLimitsModel}) => { + const mongoRepo = createMongoRepository({collection, createModel}); + + /** + * @class RealmLimitsRepository + * @extends MongoRepository + */ + return { + ...mongoRepo, + }; +}; + diff --git a/src/tasks/admin/create-realm.js b/src/tasks/admin/create-realm.js index efc7551..310d3e2 100644 --- a/src/tasks/admin/create-realm.js +++ b/src/tasks/admin/create-realm.js @@ -2,14 +2,20 @@ const {success} = require('../../lib/result'); /** * @param {RealmRepository} realmRepository The realm repository + * @param {RealmLimitsRepository} realmLimitsRepository The realm repository * @returns {AdminTasks#createRealm} Task */ -module.exports = ({realmRepository}) => +module.exports = ({realmRepository, realmLimitsRepository}) => /** * @typedef {Function} AdminTasks#createRealm * @param {object} args * @param {string} args.name The name of the realm - * @return {Promise>} Realm + * @param {object} [args.limits] The limit settings of the realm + * @param {number} [args.limits] The limit settings of the realm + * @return {Promise>} Realm */ - async ({name}) => - success(await realmRepository.create({name})); + async ({name, limits}) => { + const realm = await realmRepository.create({name}); + const realmLimits = await realmLimitsRepository.create({realmId: realm.id, ...limits}); + success({realm, realmLimits}); + } diff --git a/src/tasks/admin/index.js b/src/tasks/admin/index.js index d38089b..fc97b0c 100644 --- a/src/tasks/admin/index.js +++ b/src/tasks/admin/index.js @@ -15,6 +15,7 @@ const initListUsers = require('./list-users'); /** * @param {AuthTokenRules} authTokenRules The auth token rules * @param {RealmRepository} realmRepository The realm repository + * @param {RealmLimitsRepository} realmLimitsRepository The realm limits repository * @param {AuthRepository} authRepository The auth repository * @param {ChannelRepository} channelRepository The channel repository * @param {RealtimeConnectionRepository} realtimeConnectionRepository The real-time connection repository @@ -28,6 +29,7 @@ module.exports = ({ authRepository, channelRepository, realmRepository, + realmLimitsRepository, realtimeConnectionRepository, subscriptionRepository, userRepository, @@ -35,7 +37,7 @@ module.exports = ({ }) => ({ fetchRealm: initFetchRealm({realmRepository}), createChannel: initCreateChannel({realmRepository, channelRepository}), - createRealm: initCreateRealm({realmRepository}), + createRealm: initCreateRealm({realmRepository, realmLimitsRepository}), createRealmToken: initCreateRealmToken({authTokenRules, realmRepository, userRepository, authRepository}), createSubscription: initCreateSubscription({ userRepository,