UNPKG

@universis/user-storage

Version:

Universis Api Server Extension for managing user remote storage

366 lines (338 loc) 11.4 kB
import {ApplicationService} from '@themost/common/app'; import {AbstractMethodError, HttpBadRequestError, ConfigurationStrategy, TraceUtils, Args} from '@themost/common'; import {serviceRouter} from '@themost/express'; import path from 'path'; class UserStorageAccessConfiguration extends ConfigurationStrategy { /** * @param {ConfigurationBase} configuration */ constructor(configuration) { super(configuration); let elements = []; // define property Object.defineProperty(this, 'elements', { get: () => { return elements; }, enumerable: true }); } /** * * @param {DataContext} context - The underlying data context * @param {string} key - A string which represents the requested key * @param {string} access - A string which represents the requested access type (read or write) */ async verify(context, key, access) { // validate request context Args.notNull(context, 'Context'); // validate request context user Args.notNull(context.user, 'User'); if (context.user.authenticationScope && context.user.authenticationScope.length > 0) { // get user context scopes as array e.g, ['students', 'students:read'] let contextScopes = context.user.authenticationScope.split(','); // get user access based on HTTP method e.g. GET -> read access return this.elements.find(x => { // filter element by access level return x.access.indexOf(access) >= 0 // resource path && new RegExp("^" + x.resource, 'i').test(key) // and scopes && x.scope.find(y => { // search user scopes (validate wildcard scope) return y === "*" || contextScopes.indexOf(y) >= 0; }); }); } } } class DefaultUserStorageAccessConfiguration extends UserStorageAccessConfiguration { /** * @param {ConfigurationBase} configuration */ constructor(configuration) { super(configuration); let defaults = []; // load scope access from configuration resource try { /** * @type {Array<*>} */ defaults = require(path.resolve(configuration.getConfigurationPath(), 'user.storage.access.json')); } catch (err) { // if an error occurred other than module not found (there are no default access policies) if (err.code !== 'MODULE_NOT_FOUND') { // throw error throw err; } // otherwise continue TraceUtils.error('Default user storage access configuration cannot be found. You may configure user storage access manually or use another user storage access strategy to give user access to read or write to storage.') } this.elements.push.apply(this.elements, defaults); } } class UserStorageService extends ApplicationService { /** * Formats a path like key string to a redis command compatible key e.g. user1/application1/lastAction to .user1.application1.lastAction * @param key * @returns {string|*} */ static escapeKey(key) { let res = key.replace(/\//ig, '.'); if (/^\./.test(res)) { return res; } return '.' + res; } /** * @param {IApplication} app */ constructor(app) { super(app); // get redis options this.options = Object.assign({}, app.getConfiguration().getSourceAt('settings/universis/storage/options')); //register default user storage access configuration app.getConfiguration().useStrategy(UserStorageAccessConfiguration, DefaultUserStorageAccessConfiguration); // extend service router /** * @swagger * * /api/users/me/storage/get: * post: * tags: * - User * description: Returns a user storage item based on the specified key path * security: * - OAuth2: * - registrar * - teachers * - students * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * key: * type: string * required: * - key * responses: * '200': * description: success * content: * application/json: * schema: * type: object * '400': * description: bad request * '403': * description: forbidden * '404': * description: not found * '500': * description: internal server error */ serviceRouter.post('/users/me/storage/get', async (req, res, next) => { try { if (req.body == null) { return next(new HttpBadRequestError('Request body is missing')); } if (typeof req.body.key !== 'string') { return next(new HttpBadRequestError('Request key parameter is missing')); } const userStorage = UserStorage.create(req.context); const result = await userStorage.getItem(req.body.key); return res.json({ value: result }); } catch (err) { return next(err); } }); /** * @swagger * * /api/users/me/storage/set: * post: * tags: * - User * description: Sets a user storage item to the specified key path. * security: * - OAuth2: * - registrar * - teachers * - students * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * key: * type: string * value: * type: object * required: * - key * - value * responses: * '200': * description: success * content: * application/json: * schema: * type: object * '400': * description: bad request * '403': * description: forbidden * '404': * description: not found * '500': * description: internal server error */ serviceRouter.post('/users/me/storage/set', async (req, res, next) => { try { if (req.body == null) { return next(new HttpBadRequestError('Request body is missing')); } if (typeof req.body.key !== 'string') { return next(new HttpBadRequestError('Request key parameter is missing')); } const userStorage = UserStorage.create(req.context); // if value is null remove key let result; if (req.body.value == null) { result = await userStorage.removeItem(req.body.key); // return true if key has been removed or false if key is missing return res.json({ value: result }); } // otherwise set item result = await userStorage.setItem(req.body.key, req.body.value); return res.json({ value: result }); } catch (err) { return next(err); } }); } /** * @name RedisClient#json_set * @param {*} ...args */ /** * @name RedisClient#json_get * @param {*} ...args */ // noinspection JSMethodCanBeStatic /** * @abstract * @param {DataContext} context * @param {string} key * @param {*} value * @param {number=} expiration */ // eslint-disable-next-line no-unused-vars async setItem(context, key, value, expiration) { throw new AbstractMethodError(); } // noinspection JSMethodCanBeStatic /** * @abstract * @param {DataContext} context * @param {string} key */ // eslint-disable-next-line no-unused-vars async getItem(context, key) { throw new AbstractMethodError(); } // noinspection JSMethodCanBeStatic /** * @abstract * @param {DataContext} context * @param {string} key * @return Promise<*> */ // eslint-disable-next-line no-unused-vars async removeItem(context, key) { throw new AbstractMethodError(); } // noinspection JSMethodCanBeStatic /** * @abstract * @param {DataContext} context */ // eslint-disable-next-line no-unused-vars async clear(context) { throw new AbstractMethodError(); } } /** * @name UserStorage#context * @type {DataContext} * */ /** * @class */ class UserStorage { constructor() { } /** * @param context * @returns {UserStorage} */ static create(context) { const res = new UserStorage(); res.context = context; return res; } /** * Gets the current user storage service * @returns {UserStorageService} */ getService() { const userStorageService = this.context.getApplication().getService(UserStorageService); Args.notNull(userStorageService, UserStorageService.name); return userStorageService; } /** * Gets an item from user storage * @param key * @returns {Promise<*>} */ getItem(key) { return this.getService().getItem(this.context, key); } /** * Sets an item to user remote storage * @param {string} key * @param {*} value * @param {number=} expiration * @returns {Promise<*>} */ setItem(key, value, expiration) { return this.getService().setItem(this.context, key, value, expiration); } /** * Removes an item from user storage * @param {string} key * @returns {Promise<*>} */ removeItem(key) { return this.getService().removeItem(this.context, key); } } module.exports.UserStorageAccessConfiguration = UserStorageAccessConfiguration; module.exports.DefaultUserStorageAccessConfiguration = DefaultUserStorageAccessConfiguration; module.exports.UserStorage = UserStorage; module.exports.UserStorageService = UserStorageService;