@universis/user-storage
Version:
Universis Api Server Extension for managing user remote storage
366 lines (338 loc) • 11.4 kB
JavaScript
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;