todos-contra-el-fuego-web/packages/nimble-restivus/lib/restivus-all.js
vjrj b37069c3e5 WIP(meteor3): server BOOTS on 3.1 + Mongo 7; restivus async-patched
Major milestone: the Meteor 3.1 server boots and runs against dockerized
MongoDB 7, and the REST framework responds (status/uptime OK).

- collection2 v4: import its eager entry 'meteor/aldeed:collection2/static.js'
  from new eager files server|client/00-collection2-init.js so main.js runs and
  patches attachSchema before collections load (v4 is fully lazy, no main module).
- fixtures.js disabled: @cleverbeagle/seeder uses sync Mongo, no Meteor 3 support
  (dev fixtures, not needed for smoke). Debt.
- sitemaps.js disabled: gadicohen:sitemaps@0.0.17 pre-0.9 API, no 'sitemaps'
  global on Meteor 3. Debt (SEO).
- Vendored restivus patched for async endpoints: route handler is async and
  awaits _callEndpoint; _callEndpoint awaits the action + auth/role checks.

NEXT: Rest.js endpoints + helpers (countRealFires/firesUnion/whichAreFalsePositives/
fireFromHash/subscriptionsInsert/upsertFalsePositive) still use sync Mongo
(.count()/.findOne()/.fetch()/.insert()/.upsert()) -> convert to *Async, then
smoke test against Mongo 7.
2026-07-14 06:11:00 +02:00

1011 lines
29 KiB
JavaScript

// Vendored + precompiled from nimble:restivus 0.8.12 (CoffeeScript -> JS) so
// the app builds without the coffeescript build plugin. Self-contained `share`.
var Auth, Route, getUserQuerySelector, passwordValidator, userValidator,
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
Auth || (Auth = {});
/*
A valid user will have exactly one of the following identification fields: id, username, or email
*/
userValidator = Match.Where(function(user) {
check(user, {
id: Match.Optional(String),
username: Match.Optional(String),
email: Match.Optional(String)
});
if (_.keys(user).length === !1) {
throw new Match.Error('User must have exactly one identifier field');
}
return true;
});
/*
A password can be either in plain text or hashed
*/
passwordValidator = Match.OneOf(String, {
digest: String,
algorithm: String
});
/*
Return a MongoDB query selector for finding the given user
*/
getUserQuerySelector = function(user) {
if (user.id) {
return {
'_id': user.id
};
} else if (user.username) {
return {
'username': user.username
};
} else if (user.email) {
return {
'emails.address': user.email
};
}
throw new Error('Cannot create selector from invalid user');
};
/*
Log a user in with their password
*/
Auth.loginWithPassword = function(user, password) {
var authToken, authenticatingUser, authenticatingUserSelector, hashedToken, passwordVerification, ref;
if (!user || !password) {
throw new Meteor.Error(401, 'Unauthorized');
}
check(user, userValidator);
check(password, passwordValidator);
authenticatingUserSelector = getUserQuerySelector(user);
authenticatingUser = Meteor.users.findOne(authenticatingUserSelector);
if (!authenticatingUser) {
throw new Meteor.Error(401, 'Unauthorized');
}
if (!((ref = authenticatingUser.services) != null ? ref.password : void 0)) {
throw new Meteor.Error(401, 'Unauthorized');
}
passwordVerification = Accounts._checkPassword(authenticatingUser, password);
if (passwordVerification.error) {
throw new Meteor.Error(401, 'Unauthorized');
}
authToken = Accounts._generateStampedLoginToken();
hashedToken = Accounts._hashLoginToken(authToken.token);
Accounts._insertHashedLoginToken(authenticatingUser._id, {
hashedToken: hashedToken
});
return {
authToken: authToken.token,
userId: authenticatingUser._id
};
};
Route = (function() {
function Route(api, path1, options1, endpoints1) {
this.api = api;
this.path = path1;
this.options = options1;
this.endpoints = endpoints1;
if (!this.endpoints) {
this.endpoints = this.options;
this.options = {};
}
}
Route.prototype.addToApi = (function() {
var availableMethods;
availableMethods = ['get', 'post', 'put', 'patch', 'delete', 'options'];
return function() {
var allowedMethods, fullPath, rejectedMethods, self;
self = this;
if (_.contains(this.api._config.paths, this.path)) {
throw new Error("Cannot add a route at an existing path: " + this.path);
}
this.endpoints = _.extend({
options: this.api._config.defaultOptionsEndpoint
}, this.endpoints);
this._resolveEndpoints();
this._configureEndpoints();
this.api._config.paths.push(this.path);
allowedMethods = _.filter(availableMethods, function(method) {
return _.contains(_.keys(self.endpoints), method);
});
rejectedMethods = _.reject(availableMethods, function(method) {
return _.contains(_.keys(self.endpoints), method);
});
fullPath = this.api._config.apiPath + this.path;
_.each(allowedMethods, function(method) {
var endpoint;
endpoint = self.endpoints[method];
return JsonRoutes.add(method, fullPath, async function(req, res) {
var doneFunc, endpointContext, error, responseData, responseInitiated;
responseInitiated = false;
doneFunc = function() {
return responseInitiated = true;
};
endpointContext = {
urlParams: req.params,
queryParams: req.query,
bodyParams: req.body,
request: req,
response: res,
done: doneFunc
};
_.extend(endpointContext, endpoint);
responseData = null;
try {
// Meteor 3: endpoint actions are async (they use *Async Mongo).
responseData = await self._callEndpoint(endpointContext, endpoint);
} catch (error1) {
error = error1;
ironRouterSendErrorToResponse(error, req, res);
return;
}
if (responseInitiated) {
res.end();
return;
} else {
if (res.headersSent) {
throw new Error("Must call this.done() after handling endpoint response manually: " + method + " " + fullPath);
} else if (responseData === null || responseData === void 0) {
throw new Error("Cannot return null or undefined from an endpoint: " + method + " " + fullPath);
}
}
if (responseData.body && (responseData.statusCode || responseData.headers)) {
return self._respond(res, responseData.body, responseData.statusCode, responseData.headers);
} else {
return self._respond(res, responseData);
}
});
});
return _.each(rejectedMethods, function(method) {
return JsonRoutes.add(method, fullPath, function(req, res) {
var headers, responseData;
responseData = {
status: 'error',
message: 'API endpoint does not exist'
};
headers = {
'Allow': allowedMethods.join(', ').toUpperCase()
};
return self._respond(res, responseData, 405, headers);
});
});
};
})();
/*
Convert all endpoints on the given route into our expected endpoint object if it is a bare
function
@param {Route} route The route the endpoints belong to
*/
Route.prototype._resolveEndpoints = function() {
_.each(this.endpoints, function(endpoint, method, endpoints) {
if (_.isFunction(endpoint)) {
return endpoints[method] = {
action: endpoint
};
}
});
};
/*
Configure the authentication and role requirement on all endpoints (except OPTIONS, which must
be configured directly on the endpoint)
Authentication can be required on an entire route or individual endpoints. If required on an
entire route, that serves as the default. If required in any individual endpoints, that will
override the default.
After the endpoint is configured, all authentication and role requirements of an endpoint can be
accessed at <code>endpoint.authRequired</code> and <code>endpoint.roleRequired</code>,
respectively.
@param {Route} route The route the endpoints belong to
@param {Endpoint} endpoint The endpoint to configure
*/
Route.prototype._configureEndpoints = function() {
_.each(this.endpoints, function(endpoint, method) {
var ref, ref1;
if (method !== 'options') {
if (!((ref = this.options) != null ? ref.roleRequired : void 0)) {
this.options.roleRequired = [];
}
if (!endpoint.roleRequired) {
endpoint.roleRequired = [];
}
endpoint.roleRequired = _.union(endpoint.roleRequired, this.options.roleRequired);
if (_.isEmpty(endpoint.roleRequired)) {
endpoint.roleRequired = false;
}
if (endpoint.authRequired === void 0) {
if (((ref1 = this.options) != null ? ref1.authRequired : void 0) || endpoint.roleRequired) {
endpoint.authRequired = true;
} else {
endpoint.authRequired = false;
}
}
}
}, this);
};
/*
Authenticate an endpoint if required, and return the result of calling it
@returns The endpoint response or a 401 if authentication fails
*/
Route.prototype._callEndpoint = async function(endpointContext, endpoint) {
var auth;
auth = await this._authAccepted(endpointContext, endpoint);
if (auth.success) {
if (await this._roleAccepted(endpointContext, endpoint)) {
return await endpoint.action.call(endpointContext);
} else {
return {
statusCode: 403,
body: {
status: 'error',
message: 'You do not have permission to do this.'
}
};
}
} else {
if (auth.data) {
return auth.data;
} else {
return {
statusCode: 401,
body: {
status: 'error',
message: 'You must be logged in to do this.'
}
};
}
}
};
/*
Authenticate the given endpoint if required
Once it's globally configured in the API, authentication can be required on an entire route or
individual endpoints. If required on an entire endpoint, that serves as the default. If required
in any individual endpoints, that will override the default.
@returns An object of the following format:
{
success: Boolean
data: String or Object
}
where `success` is `true` if all required authentication checks pass and the optional `data`
will contain the auth data when successful and an optional error response when auth fails.
*/
Route.prototype._authAccepted = function(endpointContext, endpoint) {
if (endpoint.authRequired) {
return this._authenticate(endpointContext);
} else {
return {
success: true
};
}
};
/*
Verify the request is being made by an actively logged in user
If verified, attach the authenticated user to the context.
@returns An object of the following format:
{
success: Boolean
data: String or Object
}
where `success` is `true` if all required authentication checks pass and the optional `data`
will contain the auth data when successful and an optional error response when auth fails.
*/
Route.prototype._authenticate = function(endpointContext) {
var auth, userSelector;
auth = this.api._config.auth.user.call(endpointContext);
if (!auth) {
return {
success: false
};
}
if (auth.userId && auth.token && !auth.user) {
userSelector = {};
userSelector._id = auth.userId;
userSelector[this.api._config.auth.token] = auth.token;
auth.user = Meteor.users.findOne(userSelector);
}
if (auth.error) {
return {
success: false,
data: auth.error
};
}
if (auth.user) {
endpointContext.user = auth.user;
endpointContext.userId = auth.user._id;
return {
success: true,
data: auth
};
} else {
return {
success: false
};
}
};
/*
Authenticate the user role if required
Must be called after _authAccepted().
@returns True if the authenticated user belongs to <i>any</i> of the acceptable roles on the
endpoint
*/
Route.prototype._roleAccepted = function(endpointContext, endpoint) {
if (endpoint.roleRequired) {
if (_.isEmpty(_.intersection(endpoint.roleRequired, endpointContext.user.roles))) {
return false;
}
}
return true;
};
/*
Respond to an HTTP request
*/
Route.prototype._respond = function(response, body, statusCode, headers) {
var defaultHeaders, delayInMilliseconds, minimumDelayInMilliseconds, randomMultiplierBetweenOneAndTwo, sendResponse;
if (statusCode == null) {
statusCode = 200;
}
if (headers == null) {
headers = {};
}
defaultHeaders = this._lowerCaseKeys(this.api._config.defaultHeaders);
headers = this._lowerCaseKeys(headers);
headers = _.extend(defaultHeaders, headers);
if (headers['content-type'].match(/json|javascript/) !== null) {
if (this.api._config.prettyJson) {
body = JSON.stringify(body, void 0, 2);
} else {
body = JSON.stringify(body);
}
}
sendResponse = function() {
response.writeHead(statusCode, headers);
response.write(body);
return response.end();
};
if (statusCode === 401 || statusCode === 403) {
minimumDelayInMilliseconds = 500;
randomMultiplierBetweenOneAndTwo = 1 + Math.random();
delayInMilliseconds = minimumDelayInMilliseconds * randomMultiplierBetweenOneAndTwo;
return Meteor.setTimeout(sendResponse, delayInMilliseconds);
} else {
return sendResponse();
}
};
/*
Return the object with all of the keys converted to lowercase
*/
Route.prototype._lowerCaseKeys = function(object) {
return _.chain(object).pairs().map(function(attr) {
return [attr[0].toLowerCase(), attr[1]];
}).object().value();
};
return Route;
})();
Restivus = (function() {
function Restivus(options) {
var corsHeaders;
this._routes = [];
this._config = {
paths: [],
useDefaultAuth: false,
apiPath: 'api/',
version: null,
prettyJson: false,
auth: {
token: 'services.resume.loginTokens.hashedToken',
user: function() {
var token;
if (this.request.headers['x-auth-token']) {
token = Accounts._hashLoginToken(this.request.headers['x-auth-token']);
}
return {
userId: this.request.headers['x-user-id'],
token: token
};
}
},
defaultHeaders: {
'Content-Type': 'application/json'
},
enableCors: true
};
_.extend(this._config, options);
if (this._config.enableCors) {
corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept'
};
if (this._config.useDefaultAuth) {
corsHeaders['Access-Control-Allow-Headers'] += ', X-User-Id, X-Auth-Token';
}
_.extend(this._config.defaultHeaders, corsHeaders);
if (!this._config.defaultOptionsEndpoint) {
this._config.defaultOptionsEndpoint = function() {
this.response.writeHead(200, corsHeaders);
return this.done();
};
}
}
if (this._config.apiPath[0] === '/') {
this._config.apiPath = this._config.apiPath.slice(1);
}
if (_.last(this._config.apiPath) !== '/') {
this._config.apiPath = this._config.apiPath + '/';
}
if (this._config.version) {
this._config.apiPath += this._config.version + '/';
}
if (this._config.useDefaultAuth) {
this._initAuth();
} else if (this._config.useAuth) {
this._initAuth();
console.warn('Warning: useAuth API config option will be removed in Restivus v1.0 ' + '\n Use the useDefaultAuth option instead');
}
return this;
}
/**
Add endpoints for the given HTTP methods at the given path
@param path {String} The extended URL path (will be appended to base path of the API)
@param options {Object} Route configuration options
@param options.authRequired {Boolean} The default auth requirement for each endpoint on the route
@param options.roleRequired {String or String[]} The default role required for each endpoint on the route
@param endpoints {Object} A set of endpoints available on the new route (get, post, put, patch, delete, options)
@param endpoints.<method> {Function or Object} If a function is provided, all default route
configuration options will be applied to the endpoint. Otherwise an object with an `action`
and all other route config options available. An `action` must be provided with the object.
*/
Restivus.prototype.addRoute = function(path, options, endpoints) {
var route;
route = new Route(this, path, options, endpoints);
this._routes.push(route);
route.addToApi();
return this;
};
/**
Generate routes for the Meteor Collection with the given name
*/
Restivus.prototype.addCollection = function(collection, options) {
var collectionEndpoints, collectionRouteEndpoints, endpointsAwaitingConfiguration, entityRouteEndpoints, excludedEndpoints, methods, methodsOnCollection, path, routeOptions;
if (options == null) {
options = {};
}
methods = ['get', 'post', 'put', 'patch', 'delete', 'getAll'];
methodsOnCollection = ['post', 'getAll'];
if (collection === Meteor.users) {
collectionEndpoints = this._userCollectionEndpoints;
} else {
collectionEndpoints = this._collectionEndpoints;
}
endpointsAwaitingConfiguration = options.endpoints || {};
routeOptions = options.routeOptions || {};
excludedEndpoints = options.excludedEndpoints || [];
path = options.path || collection._name;
collectionRouteEndpoints = {};
entityRouteEndpoints = {};
if (_.isEmpty(endpointsAwaitingConfiguration) && _.isEmpty(excludedEndpoints)) {
_.each(methods, function(method) {
if (indexOf.call(methodsOnCollection, method) >= 0) {
_.extend(collectionRouteEndpoints, collectionEndpoints[method].call(this, collection));
} else {
_.extend(entityRouteEndpoints, collectionEndpoints[method].call(this, collection));
}
}, this);
} else {
_.each(methods, function(method) {
var configuredEndpoint, endpointOptions;
if (indexOf.call(excludedEndpoints, method) < 0 && endpointsAwaitingConfiguration[method] !== false) {
endpointOptions = endpointsAwaitingConfiguration[method];
configuredEndpoint = {};
_.each(collectionEndpoints[method].call(this, collection), function(action, methodType) {
return configuredEndpoint[methodType] = _.chain(action).clone().extend(endpointOptions).value();
});
if (indexOf.call(methodsOnCollection, method) >= 0) {
_.extend(collectionRouteEndpoints, configuredEndpoint);
} else {
_.extend(entityRouteEndpoints, configuredEndpoint);
}
}
}, this);
}
this.addRoute(path, routeOptions, collectionRouteEndpoints);
this.addRoute(path + "/:id", routeOptions, entityRouteEndpoints);
return this;
};
/**
A set of endpoints that can be applied to a Collection Route
*/
Restivus.prototype._collectionEndpoints = {
get: function(collection) {
return {
get: {
action: function() {
var entity;
entity = collection.findOne(this.urlParams.id);
if (entity) {
return {
status: 'success',
data: entity
};
} else {
return {
statusCode: 404,
body: {
status: 'fail',
message: 'Item not found'
}
};
}
}
}
};
},
put: function(collection) {
return {
put: {
action: function() {
var entity, entityIsUpdated;
entityIsUpdated = collection.update(this.urlParams.id, this.bodyParams);
if (entityIsUpdated) {
entity = collection.findOne(this.urlParams.id);
return {
status: 'success',
data: entity
};
} else {
return {
statusCode: 404,
body: {
status: 'fail',
message: 'Item not found'
}
};
}
}
}
};
},
patch: function(collection) {
return {
patch: {
action: function() {
var entity, entityIsUpdated;
entityIsUpdated = collection.update(this.urlParams.id, {
$set: this.bodyParams
});
if (entityIsUpdated) {
entity = collection.findOne(this.urlParams.id);
return {
status: 'success',
data: entity
};
} else {
return {
statusCode: 404,
body: {
status: 'fail',
message: 'Item not found'
}
};
}
}
}
};
},
"delete": function(collection) {
return {
"delete": {
action: function() {
if (collection.remove(this.urlParams.id)) {
return {
status: 'success',
data: {
message: 'Item removed'
}
};
} else {
return {
statusCode: 404,
body: {
status: 'fail',
message: 'Item not found'
}
};
}
}
}
};
},
post: function(collection) {
return {
post: {
action: function() {
var entity, entityId;
entityId = collection.insert(this.bodyParams);
entity = collection.findOne(entityId);
if (entity) {
return {
statusCode: 201,
body: {
status: 'success',
data: entity
}
};
} else {
return {
statusCode: 400,
body: {
status: 'fail',
message: 'No item added'
}
};
}
}
}
};
},
getAll: function(collection) {
return {
get: {
action: function() {
var entities;
entities = collection.find().fetch();
if (entities) {
return {
status: 'success',
data: entities
};
} else {
return {
statusCode: 404,
body: {
status: 'fail',
message: 'Unable to retrieve items from collection'
}
};
}
}
}
};
}
};
/**
A set of endpoints that can be applied to a Meteor.users Collection Route
*/
Restivus.prototype._userCollectionEndpoints = {
get: function(collection) {
return {
get: {
action: function() {
var entity;
entity = collection.findOne(this.urlParams.id, {
fields: {
profile: 1
}
});
if (entity) {
return {
status: 'success',
data: entity
};
} else {
return {
statusCode: 404,
body: {
status: 'fail',
message: 'User not found'
}
};
}
}
}
};
},
put: function(collection) {
return {
put: {
action: function() {
var entity, entityIsUpdated;
entityIsUpdated = collection.update(this.urlParams.id, {
$set: {
profile: this.bodyParams
}
});
if (entityIsUpdated) {
entity = collection.findOne(this.urlParams.id, {
fields: {
profile: 1
}
});
return {
status: "success",
data: entity
};
} else {
return {
statusCode: 404,
body: {
status: 'fail',
message: 'User not found'
}
};
}
}
}
};
},
"delete": function(collection) {
return {
"delete": {
action: function() {
if (collection.remove(this.urlParams.id)) {
return {
status: 'success',
data: {
message: 'User removed'
}
};
} else {
return {
statusCode: 404,
body: {
status: 'fail',
message: 'User not found'
}
};
}
}
}
};
},
post: function(collection) {
return {
post: {
action: function() {
var entity, entityId;
entityId = Accounts.createUser(this.bodyParams);
entity = collection.findOne(entityId, {
fields: {
profile: 1
}
});
if (entity) {
return {
statusCode: 201,
body: {
status: 'success',
data: entity
}
};
} else {
({
statusCode: 400
});
return {
status: 'fail',
message: 'No user added'
};
}
}
}
};
},
getAll: function(collection) {
return {
get: {
action: function() {
var entities;
entities = collection.find({}, {
fields: {
profile: 1
}
}).fetch();
if (entities) {
return {
status: 'success',
data: entities
};
} else {
return {
statusCode: 404,
body: {
status: 'fail',
message: 'Unable to retrieve users'
}
};
}
}
}
};
}
};
/*
Add /login and /logout endpoints to the API
*/
Restivus.prototype._initAuth = function() {
var logout, self;
self = this;
/*
Add a login endpoint to the API
After the user is logged in, the onLoggedIn hook is called (see Restfully.configure() for
adding hook).
*/
this.addRoute('login', {
authRequired: false
}, {
post: function() {
var auth, e, extraData, password, ref, ref1, response, searchQuery, user;
user = {};
if (this.bodyParams.user) {
if (this.bodyParams.user.indexOf('@') === -1) {
user.username = this.bodyParams.user;
} else {
user.email = this.bodyParams.user;
}
} else if (this.bodyParams.username) {
user.username = this.bodyParams.username;
} else if (this.bodyParams.email) {
user.email = this.bodyParams.email;
}
password = this.bodyParams.password;
if (this.bodyParams.hashed) {
password = {
digest: password,
algorithm: 'sha-256'
};
}
try {
auth = Auth.loginWithPassword(user, password);
} catch (error1) {
e = error1;
return {
statusCode: e.error,
body: {
status: 'error',
message: e.reason
}
};
}
if (auth.userId && auth.authToken) {
searchQuery = {};
searchQuery[self._config.auth.token] = Accounts._hashLoginToken(auth.authToken);
this.user = Meteor.users.findOne({
'_id': auth.userId
}, searchQuery);
this.userId = (ref = this.user) != null ? ref._id : void 0;
}
response = {
status: 'success',
data: auth
};
extraData = (ref1 = self._config.onLoggedIn) != null ? ref1.call(this) : void 0;
if (extraData != null) {
_.extend(response.data, {
extra: extraData
});
}
return response;
}
});
logout = function() {
var authToken, extraData, hashedToken, index, ref, response, tokenFieldName, tokenLocation, tokenPath, tokenRemovalQuery, tokenToRemove;
authToken = this.request.headers['x-auth-token'];
hashedToken = Accounts._hashLoginToken(authToken);
tokenLocation = self._config.auth.token;
index = tokenLocation.lastIndexOf('.');
tokenPath = tokenLocation.substring(0, index);
tokenFieldName = tokenLocation.substring(index + 1);
tokenToRemove = {};
tokenToRemove[tokenFieldName] = hashedToken;
tokenRemovalQuery = {};
tokenRemovalQuery[tokenPath] = tokenToRemove;
Meteor.users.update(this.user._id, {
$pull: tokenRemovalQuery
});
response = {
status: 'success',
data: {
message: 'You\'ve been logged out!'
}
};
extraData = (ref = self._config.onLoggedOut) != null ? ref.call(this) : void 0;
if (extraData != null) {
_.extend(response.data, {
extra: extraData
});
}
return response;
};
/*
Add a logout endpoint to the API
After the user is logged out, the onLoggedOut hook is called (see Restfully.configure() for
adding hook).
*/
return this.addRoute('logout', {
authRequired: true
}, {
get: function() {
console.warn("Warning: Default logout via GET will be removed in Restivus v1.0. Use POST instead.");
console.warn(" See https://github.com/kahmali/meteor-restivus/issues/100");
return logout.call(this);
},
post: logout
});
};
return Restivus;
})();
Restivus = Restivus;