define([
"jquery",
"underscore",
"backbone",
"jws",
"models/Search",
"collections/SolrResults",
], ($, _, Backbone, JWS, SearchModel, SearchResults) => {
"use strict";
/**
* @class UserModel
* @classcategory Models
* @augments Backbone.Model
* @class
*/
const UserModel = Backbone.Model.extend(
/** @lends UserModel.prototype */ {
defaults() {
return {
type: "person", // assume this is a person unless we are told otherwise - other possible type is a "group"
checked: false, // Is set to true when we have checked the account/subject info of this user
tokenChecked: false, // Is set to true when the uer auth token has been checked
basicUser: false, // Set to true to only query for basic info about this user - prevents sending queries for info that will never be displayed in the UI
lastName: null,
firstName: null,
fullName: null,
email: null,
logo: null,
description: null,
verified: null,
username: null,
usernameReadable: null,
orcid: null,
searchModel: null,
searchResults: null,
loggedIn: false,
ldapError: false, // Was there an error logging in to LDAP
registered: false,
isMemberOf: [],
isOwnerOf: [],
identities: [],
identitiesUsernames: [],
allIdentitiesAndGroups: [],
pending: [],
token: null,
expires: null,
timeoutId: null,
rawData: null,
portalQuota: -1,
isAuthorizedCreatePortal: null,
dataoneQuotas: null,
dataoneSubscription: null,
};
},
initialize(options) {
if (typeof options !== "undefined") {
if (options.username) this.set("username", options.username);
if (options.rawData) this.set(this.parseXML(options.rawData));
}
this.on("change:identities", this.pluckIdentityUsernames);
this.on(
"change:username change:identities change:type",
this.updateSearchModel,
);
this.createSearchModel();
this.on("change:username", this.createReadableUsername());
// Create a search results model for this person
const searchResults = new SearchResults([], { rows: 5, start: 0 });
this.set("searchResults", searchResults);
if (MetacatUI.appModel.get("enableBookkeeperServices")) {
// When the user is logged in, see if they have a DataONE subscription
this.on("change:loggedIn", this.fetchSubscription);
}
},
createSearchModel() {
// Create a search model that will retrieve data created by this person
this.set("searchModel", new SearchModel());
this.updateSearchModel();
},
updateSearchModel() {
if (this.get("type") == "node") {
this.get("searchModel").set("dataSource", [
this.get("node").identifier,
]);
this.get("searchModel").set("username", []);
} else {
// Get all the identities for this person
const ids = [this.get("username")];
_.each(this.get("identities"), (equivalentUser) => {
ids.push(equivalentUser.get("username"));
});
this.get("searchModel").set("username", ids);
}
this.trigger("change:searchModel");
},
parseXML(data) {
let username = this.get("username");
// Reset the group list so we don't just add it to it with push()
this.set("isMemberOf", this.defaults().isMemberOf, { silent: true });
this.set("isOwnerOf", this.defaults().isOwnerOf, { silent: true });
// Reset the equivalent id list so we don't just add it to it with push()
this.set("identities", this.defaults().identities, { silent: true });
// Find this person's node in the XML
let userNode = null;
if (!username) username = $(data).children("subject").text();
if (username) {
const subjects = $(data).find("subject");
for (let i = 0; i < subjects.length; i++) {
if (
$(subjects[i]).text().toLowerCase() === username.toLowerCase()
) {
userNode = $(subjects[i]).parent();
break;
}
}
}
if (!userNode) userNode = $(data).first();
// Get the type of user - either a person or group
let type = $(userNode).prop("tagName");
if (type) type = type.toLowerCase();
if (type == "group") {
var fullName = $(userNode).find("groupName").first().text();
} else if (type) {
// Find the person's info
var firstName = $(userNode).find("givenName").first().text();
var lastName = $(userNode).find("familyName").first().text();
var email = $(userNode).find("email").first().text();
var verified = $(userNode).find("verified").first().text();
var memberOf = this.get("isMemberOf");
var ownerOf = this.get("isOwnerOf");
var identities = this.get("identities");
var equivUsernames = [];
// Sometimes names are saved as "NA" when they are not available - translate these to false values
if (firstName == "NA") firstName = null;
if (lastName == "NA") lastName = null;
// Construct the fullname from the first and last names, but watch out for falsely values
var fullName = "";
fullName += firstName || "";
fullName += lastName ? ` ${lastName}` : "";
if (!fullName) fullName = this.getNameFromSubject(username);
// Don't get this detailed info about basic users
if (!this.get("basicUser")) {
// Get all the equivalent identities for this user
const equivalentIds = $(userNode).find("equivalentIdentity");
if (equivalentIds.length > 0)
var allPersons = $(data).find("person subject");
_.each(equivalentIds, (identity, i) => {
// push onto the list
const username = $(identity).text();
let equivUserNode;
// Find the matching person node in the response
_.each(allPersons, (person) => {
if ($(person).text().toLowerCase() == username.toLowerCase()) {
equivUserNode = $(person).parent().first();
allPersons = _.without(allPersons, person);
}
});
const equivalentUser = new UserModel({
username,
basicUser: true,
rawData: equivUserNode,
});
identities.push(equivalentUser);
equivUsernames.push(username);
});
}
// Get each group and save
_.each($(data).find("group"), (group, i) => {
// Save group ID
const groupId = $(group).find("subject").first().text();
const groupName = $(group).find("groupName").text();
memberOf.push({ groupId, name: groupName });
// Check if this person is a rightsholder
const allRightsHolders = [];
_.each($(group).children("rightsHolder"), (rightsHolder) => {
allRightsHolders.push($(rightsHolder).text().toLowerCase());
});
if (_.contains(allRightsHolders, username.toLowerCase()))
ownerOf.push(groupId);
});
}
let allSubjects = _.pluck(this.get("isMemberOf"), "groupId");
allSubjects.push(this.get("username"));
allSubjects = allSubjects.concat(equivUsernames);
return {
isMemberOf: memberOf,
isOwnerOf: ownerOf,
identities,
allIdentitiesAndGroups: allSubjects,
verified,
username,
firstName,
lastName,
fullName,
email,
registered: true,
type,
rawData: data,
};
},
getInfo() {
const model = this;
// If the accounts service is not on, flag this user as checked/completed
if (!MetacatUI.appModel.get("accountsUrl")) {
this.set("fullName", this.getNameFromSubject());
this.set("checked", true);
return;
}
// Only proceed if there is a username
if (!this.get("username")) return;
// Get the user info using the DataONE API
const url =
MetacatUI.appModel.get("accountsUrl") +
encodeURIComponent(this.get("username"));
const requestSettings = {
type: "GET",
url,
success(data, textStatus, xhr) {
// Parse the XML response to get user info
const userProperties = model.parseXML(data);
// Filter out all the falsey values
_.each(userProperties, (v, k) => {
if (!v) {
delete userProperties[k];
}
});
model.set(userProperties);
// Trigger the change events
model.trigger("change:isMemberOf");
model.trigger("change:isOwnerOf");
model.trigger("change:identities");
model.set("checked", true);
},
error(xhr, textStatus, errorThrown) {
// Sometimes the node info has not been received before this getInfo() is called.
// If the node info was received while this getInfo request was pending, and this user was determined
// to be a node, then we can skip any further action here.
if (model.get("type") == "node") return;
if (xhr.status == 404 && MetacatUI.nodeModel.get("checked")) {
model.set("fullName", model.getNameFromSubject());
model.set("checked", true);
} else if (
xhr.status == 404 &&
!MetacatUI.nodeModel.get("checked")
) {
model.listenToOnce(MetacatUI.nodeModel, "change:checked", () => {
if (!model.isNode()) {
model.set("fullName", model.getNameFromSubject());
model.set("checked", true);
}
});
} else {
// As a backup, search for this user instead
const requestSettings = {
type: "GET",
url: `${MetacatUI.appModel.get(
"accountsUrl",
)}?query=${encodeURIComponent(model.get("username"))}`,
success(data, textStatus, xhr) {
// Parse the XML response to get user info
model.set(model.parseXML(data));
// Trigger the change events
model.trigger("change:isMemberOf");
model.trigger("change:isOwnerOf");
model.trigger("change:identities");
model.set("checked", true);
},
error() {
// Set some blank values and flag as checked
// model.set("username", "");
// model.set("fullName", "");
model.set("notFound", true);
model.set("checked", true);
},
};
// Send the request
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
}
},
};
// Send the request
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
// Get the pending identity map requests, if the service is turned on
getPendingIdentities() {
if (!MetacatUI.appModel.get("pendingMapsUrl")) return false;
const model = this;
// Get the pending requests
const requestSettings = {
url:
MetacatUI.appModel.get("pendingMapsUrl") +
encodeURIComponent(this.get("username")),
success(data, textStatus, xhr) {
// Reset the equivalent id list so we don't just add it to it with push()
model.set("pending", model.defaults().pending);
const pending = model.get("pending");
_.each($(data).find("person"), (person, i) => {
// Don't list yourself as a pending map request
const personsUsername = $(person).find("subject").text();
if (
personsUsername.toLowerCase() ==
model.get("username").toLowerCase()
)
return;
// Create a new User Model for this pending identity
const pendingUser = new UserModel({ rawData: person });
if (pendingUser.isOrcid()) pendingUser.getInfo();
pending.push(pendingUser);
});
model.set("pending", pending);
model.trigger("change:pending"); // Trigger the change event
},
error(xhr, textStatus) {
if (xhr.responseText.indexOf("error code 34")) {
model.set("pending", model.defaults().pending);
model.trigger("change:pending"); // Trigger the change event
}
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
getNameFromSubject(username) {
var username = username || this.get("username");
let fullName = "";
if (!username) return;
if (username.indexOf("uid=") > -1 && username.indexOf(",") > -1)
fullName = username.substring(
username.indexOf("uid=") + 4,
username.indexOf(","),
);
else if (username.indexOf("CN=") > -1 && username.indexOf(",") > -1)
fullName = username.substring(
username.indexOf("CN=") + 3,
username.indexOf(","),
);
// Cut off the last string after the name when it contains digits - not part of this person's names
if (fullName.lastIndexOf(" ") > fullName.indexOf(" ")) {
const lastWord = fullName.substring(fullName.lastIndexOf(" "));
if (lastWord.search(/\d/) > -1)
fullName = fullName.substring(0, fullName.lastIndexOf(" "));
}
// Default to the username
if (!fullName) fullName = this.get("fullname") || username;
return fullName;
},
isOrcid(orcid) {
const username =
typeof orcid === "string" ? orcid : this.get("username");
// Have we already verified this?
if (typeof orcid === "undefined" && username == this.get("orcid"))
return true;
// Checks for ORCIDs using the orcid base URL as a prefix
if (username.indexOf("orcid.org/") > -1) {
return true;
}
// If the ORCID base url is not present, we will check if this is a 19-digit ORCID ID
// A simple and fast check first
// ORCiDs are 16 digits and 3 dashes - 19 characters
if (username.length != 19) return false;
/* The ORCID checksum algorithm to determine is a character string is an ORCiD
* http://support.orcid.org/knowledgebase/articles/116780-structure-of-the-orcid-identifier
*/
let total = 0;
const baseDigits = username.replace(/-/g, "").substr(0, 15);
for (let i = 0; i < baseDigits.length; i++) {
const digit = parseInt(baseDigits.charAt(i));
total = (total + digit) * 2;
}
const remainder = total % 11;
const result = (12 - remainder) % 11;
const checkDigit = result == 10 ? "X" : result.toString();
const isOrcid = checkDigit == username.charAt(username.length - 1);
if (isOrcid) this.set("orcid", username);
return isOrcid;
},
isNode() {
const model = this;
const node = _.find(
MetacatUI.nodeModel.get("members"),
(nodeModel) =>
nodeModel.shortIdentifier.toLowerCase() ==
model.get("username").toLowerCase(),
);
return node && node !== undefined;
},
// Will check if this user is a Member Node. If so, it will save the MN info to the model
saveAsNode() {
if (!this.isNode()) return;
const model = this;
const node = _.find(
MetacatUI.nodeModel.get("members"),
(nodeModel) =>
nodeModel.shortIdentifier.toLowerCase() ==
model.get("username").toLowerCase(),
);
this.set({
type: "node",
logo: node.logo,
description: node.description,
node,
fullName: node.name,
usernameReadable: this.get("username"),
});
this.updateSearchModel();
this.set("checked", true);
},
loginLdap(formData, success, error) {
if (!formData || !appModel.get("signInUrlLdap")) return false;
const model = this;
const requestSettings = {
type: "POST",
url: MetacatUI.appModel.get("signInUrlLdap") + window.location.href,
data: formData,
success(data, textStatus, xhr) {
if (success) success(this);
model.getToken();
},
error() {
/* if(error)
error(this);
*/
model.getToken();
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
logout() {
// Construct the sign out url and redirect
let signOutUrl = MetacatUI.appModel.get("signOutUrl");
let target = Backbone.history.location.href;
// DO NOT include the route otherwise we have an infinite redirect
// target = target.split("#")[0];
target = target.slice(0, -8);
// make sure to include the target
signOutUrl += `?target=${target}`;
// do it!
window.location.replace(signOutUrl);
},
// call Metacat or the DataONE CN to validate the session and tell us the user's name
checkStatus(onSuccess, onError) {
const model = this;
if (!MetacatUI.appModel.get("tokenUrl")) {
// look up the URL
const metacatUrl = MetacatUI.appModel.get("metacatServiceUrl");
// ajax call to validate the session/get the user info
const requestSettings = {
type: "POST",
url: metacatUrl,
data: { action: "validatesession" },
success(data, textStatus, xhr) {
// the Metacat (XML) response should have a fullName element
const username = $(data).find("name").text();
// set in the model
model.set("username", username);
// Are we logged in?
if (username) {
model.set("loggedIn", true);
model.getInfo();
} else {
model.set("loggedIn", false);
model.trigger("change:loggedIn");
model.set("checked", true);
}
if (onSuccess) onSuccess(data);
},
error(data, textStatus, xhr) {
// User is not logged in
model.reset();
if (onError) onError();
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
} else {
// use the token method for checking authentication
this.getToken();
}
},
getToken(customCallback) {
this.set("checked", false);
this.set("tokenChecked", false);
this.set("error", null);
const tokenUrl = MetacatUI.appModel.get("tokenUrl");
const model = this;
if (!tokenUrl) return false;
// Set up the function that will be called when we retrieve a token
const callback =
typeof customCallback === "function"
? customCallback
: function (data, textStatus, xhr) {
// the response should have the token
const payload = model.parseToken(data);
const username = payload ? payload.userId : null;
const fullName = payload
? payload.fullName
: model.getNameFromSubject(username) || null;
const token = payload ? data : null;
const loggedIn = !!payload;
// set in the model
model.set("fullName", fullName);
model.set("username", username);
model.set("token", token);
model.set("loggedIn", loggedIn);
model.set("tokenChecked", true);
model.getTokenExpiration(payload);
if (username) model.getInfo();
else model.set("checked", true);
};
// ajax call to get token
const requestSettings = {
type: "GET",
dataType: "text",
xhrFields: {
withCredentials: true,
},
url: tokenUrl,
data: {},
success: callback,
error(xhr, textStatus, errorThrown) {
model.set("checked", true);
model.set("error", textStatus);
},
};
$.ajax(requestSettings);
},
/**
* Returns a promise that resolves with the token when it is retrieved.
* @param {number} [timeout] - The time in milliseconds to wait for the
* token before rejecting the promise.
* @returns {Promise<string>} A promise that resolves with the token or
* rejects with an error message.
*/
getTokenPromise(timeout = 5000) {
const model = this;
return new Promise((resolve, reject) => {
const listenModel = new Backbone.Model();
const stopListenModel = () => {
listenModel.stopListening();
listenModel.destroy();
};
listenModel.listenToOnce(model, "change:error", () => {
stopListenModel();
reject(model.get("error"));
});
listenModel.listenToOnce(model, "change:tokenChecked", () => {
stopListenModel();
resolve(model.get("token"));
});
if (timeout) {
setTimeout(() => {
stopListenModel();
reject(new Error("token check timed out"));
}, timeout);
}
model.getToken();
});
},
getTokenExpiration(payload) {
if (!payload && this.get("token"))
var payload = this.parseToken(this.get("token"));
if (!payload) return;
// The exp claim should be standard - it is in UTC seconds
let expires = payload.exp ? new Date(payload.exp * 1000) : null;
// Use the issuedAt and ttl as a backup (only used in d1 2.0.0 and 2.0.1)
if (!expires) {
const issuedAt = payload.issuedAt ? new Date(payload.issuedAt) : null;
const lifeSpan = payload.ttl ? payload.ttl : null;
if (issuedAt && lifeSpan && lifeSpan > 99999)
issuedAt.setMilliseconds(lifeSpan);
else if (issuedAt && lifeSpan) issuedAt.setSeconds(lifeSpan);
expires = issuedAt;
}
this.set("expires", expires);
},
checkToken(onSuccess, onError) {
// First check if the token has expired
if (MetacatUI.appUserModel.get("expires") > new Date()) {
if (onSuccess) onSuccess();
return;
}
const model = this;
const url = MetacatUI.appModel.get("tokenUrl");
if (!url) return;
const requestSettings = {
type: "GET",
url,
headers: {
"Cache-Control": "no-cache",
},
success(data, textStatus, xhr) {
if (data) {
// the response should have the token
const payload = model.parseToken(data);
const username = payload ? payload.userId : null;
const fullName = payload ? payload.fullName : null;
const token = payload ? data : null;
const loggedIn = !!payload;
// set in the model
model.set("fullName", fullName);
model.set("username", username);
model.set("token", token);
model.set("loggedIn", loggedIn);
model.getTokenExpiration(payload);
MetacatUI.appUserModel.set("checked", true);
if (onSuccess) onSuccess(data, textStatus, xhr);
} else if (onError) onError(data, textStatus, xhr);
},
error(data, textStatus, xhr) {
// If this token in invalid, then reset the user model/log out
MetacatUI.appUserModel.reset();
if (onError) onError(data, textStatus, xhr);
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
parseToken(token) {
if (typeof token === "undefined") var token = this.get("token");
const jws = new KJUR.jws.JWS();
let result = 0;
try {
result = jws.parseJWS(token);
} catch (ex) {
result = 0;
}
if (!jws.parsedJWS) return "";
const payload = $.parseJSON(jws.parsedJWS.payloadS);
return payload;
},
update(onSuccess, onError) {
const model = this;
const person =
`<?xml version="1.0" encoding="UTF-8"?>` +
`<d1:person xmlns:d1="http://ns.dataone.org/service/types/v1">` +
`<subject>${this.get("username")}</subject>` +
`<givenName>${this.get("firstName")}</givenName>` +
`<familyName>${this.get("lastName")}</familyName>` +
`<email>${this.get("email")}</email>` +
`</d1:person>`;
const xmlBlob = new Blob([person], { type: "application/xml" });
const formData = new FormData();
formData.append("subject", this.get("username"));
formData.append("person", xmlBlob, "person");
const updateUrl =
MetacatUI.appModel.get("accountsUrl") +
encodeURIComponent(this.get("username"));
// ajax call to update
const requestSettings = {
type: "PUT",
cache: false,
contentType: false,
processData: false,
url: updateUrl,
data: formData,
success(data, textStatus, xhr) {
if (typeof onSuccess !== "undefined") onSuccess(data);
// model.getInfo();
},
error(data, textStatus, xhr) {
if (typeof onError !== "undefined") onError(data);
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
confirmMapRequest(otherUsername, onSuccess, onError) {
if (!otherUsername) return;
const mapUrl =
MetacatUI.appModel.get("pendingMapsUrl") +
encodeURIComponent(otherUsername);
const model = this;
if (!onSuccess) var onSuccess = function () {};
if (!onError) var onError = function () {};
// ajax call to confirm map
const requestSettings = {
type: "PUT",
url: mapUrl,
success(data, textStatus, xhr) {
if (onSuccess) onSuccess(data, textStatus, xhr);
// Get updated info
model.getInfo();
},
error(xhr, textStatus, error) {
if (onError) onError(xhr, textStatus, error);
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
denyMapRequest(otherUsername, onSuccess, onError) {
if (!otherUsername) return;
const mapUrl =
MetacatUI.appModel.get("pendingMapsUrl") +
encodeURIComponent(otherUsername);
const model = this;
// ajax call to reject map
const requestSettings = {
type: "DELETE",
url: mapUrl,
success(data, textStatus, xhr) {
if (typeof onSuccess === "function")
onSuccess(data, textStatus, xhr);
model.getInfo();
},
error(xhr, textStatus, error) {
if (typeof onError === "function") onError(xhr, textStatus, error);
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
addMap(otherUsername, onSuccess, onError) {
if (!otherUsername) return;
let mapUrl = MetacatUI.appModel.get("pendingMapsUrl");
const model = this;
if (mapUrl.charAt(mapUrl.length - 1) == "/") {
mapUrl = mapUrl.substring(0, mapUrl.length - 1);
}
// ajax call to map
const requestSettings = {
type: "POST",
xhrFields: {
withCredentials: true,
},
headers: {
Authorization: `Bearer ${this.get("token")}`,
},
url: mapUrl,
data: {
subject: otherUsername,
},
success(data, textStatus, xhr) {
if (typeof onSuccess === "function")
onSuccess(data, textStatus, xhr);
model.getInfo();
},
error(xhr, textStatus, error) {
// Check if the username might have been spelled or formatted incorrectly
// ORCIDs, in particular, have different formats that we should account for
if (
xhr.responseText.indexOf("LDAP: error code 32 - No Such Object") >
-1 &&
model.isOrcid(otherUsername)
) {
if (otherUsername.length == 19)
model.addMap(
`http://orcid.org/${otherUsername}`,
onSuccess,
onError,
);
else if (otherUsername.indexOf("https://orcid.org") == 0)
model.addMap(
otherUsername.replace("https", "http"),
onSuccess,
onError,
);
else if (otherUsername.indexOf("orcid.org") == 0)
model.addMap(`http://${otherUsername}`, onSuccess, onError);
else if (otherUsername.indexOf("www.orcid.org") == 0)
model.addMap(
otherUsername.replace("www.", "http://"),
onSuccess,
onError,
);
else if (otherUsername.indexOf("http://www.orcid.org") == 0)
model.addMap(
otherUsername.replace("www.", ""),
onSuccess,
onError,
);
else if (otherUsername.indexOf("https://www.orcid.org") == 0)
model.addMap(
otherUsername.replace("https://www.", "http://"),
onSuccess,
onError,
);
else if (typeof onError === "function")
onError(xhr, textStatus, error);
} else if (typeof onError === "function")
onError(xhr, textStatus, error);
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
removeMap(otherUsername, onSuccess, onError) {
if (!otherUsername) return;
const mapUrl =
MetacatUI.appModel.get("accountsMapsUrl") +
encodeURIComponent(otherUsername);
const model = this;
// ajax call to remove mapping
const requestSettings = {
type: "DELETE",
url: mapUrl,
success(data, textStatus, xhr) {
if (typeof onSuccess === "function")
onSuccess(data, textStatus, xhr);
model.getInfo();
},
error(xhr, textStatus, error) {
if (typeof onError === "function") onError(xhr, textStatus, error);
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
failedLdapLogin() {
this.set("loggedIn", false);
this.set("checked", true);
this.set("ldapError", true);
},
pluckIdentityUsernames() {
const models = this.get("identities");
const usernames = [];
_.each(models, (m) => {
usernames.push(m.get("username").toLowerCase());
});
this.set("identitiesUsernames", usernames);
this.trigger("change:identitiesUsernames");
},
createReadableUsername() {
if (!this.get("username")) return;
const username = this.get("username");
const readableUsername =
username.substring(
username.indexOf("=") + 1,
username.indexOf(","),
) || username;
this.set("usernameReadable", readableUsername);
},
createAjaxSettings() {
if (!this.get("token")) return {};
return {
xhrFields: {
withCredentials: true,
},
headers: {
Authorization: `Bearer ${this.get("token")}`,
},
};
},
/**
* Creates the settings object to be used in a fetch() call to the DataONE
* API
* @returns {object} The settings object to be passed to the fetch()
* function
*/
createFetchSettings() {
const token = this.get("token");
if (!token) return {};
return {
credentials: "include",
headers: {
Authorization: `Bearer ${token}`,
},
};
},
/**
* Checks if this user has the quota to perform the given action
* @param {string} action - The action to be performed
* @param {string} customerGroup - The subject or identifier of the customer/membership group
* to use this quota against
*/
checkQuota(action, customerGroup) {
// Temporarily reset the quota so a trigger event is changed when the XHR is complete
this.set("portalQuota", -1, { silent: true });
// Start of temporary code
// TODO: Replace this function with real code once the quota service is working
this.set("portalQuota", 999);
// End of temporary code
/* var model = this;
var requestSettings = {
url: "",
type: "GET",
success: function(data, textStatus, xhr) {
model.set("portalQuota", data.remainingQuota);
},
error: function(xhr, textStatus, errorThrown) {
model.set("portalQuota", 0);
}
}
$.ajax(_.extend(requestSettings, this.createAjaxSettings()));
*/
},
/**
* Checks if the user has authorization to perform the given action.
*/
isAuthorizedCreatePortal() {
// Reset the isAuthorized attribute silently so a change event is always triggered
this.set("isAuthorizedCreatePortal", null, { silent: true });
// If the user isn't logged in, set authorization to false
if (!this.get("loggedIn")) {
this.set("isAuthorizedCreatePortal", false);
return;
}
// If creating portals has been disabled app-wide, then set to false
if (MetacatUI.appModel.get("enableCreatePortals") === false) {
this.set("isAuthorizedCreatePortal", false);
}
// If creating portals has been limited to only certain subjects, check if this user is one of them
else if (MetacatUI.appModel.get("limitPortalsToSubjects").length) {
if (!this.get("allIdentitiesAndGroups").length) {
this.on(
"change:allIdentitiesAndGroups",
this.isAuthorizedCreatePortal,
);
return;
}
// Find the subjects that have access to create portals. Could be specific users or groups.
const subjectsThatHaveAccess = _.intersection(
MetacatUI.appModel.get("limitPortalsToSubjects"),
this.get("allIdentitiesAndGroups"),
);
if (!subjectsThatHaveAccess.length) {
// If this user is not in the whitelist, set to false
this.set("isAuthorizedCreatePortal", false);
} else {
// If this user is in the whitelist, set to true
this.set("isAuthorizedCreatePortal", true);
}
}
// If anyone is allowed to create a portal, check if they have the quota to create a portal
else if (MetacatUI.appModel.get("enableBookkeeperServices")) {
// Get the Quotas for this user
const quotas = this.get("dataoneQuotas");
// If the Quotas are still being fetched,
if (quotas == this.defaults().dataoneQuotas && !quotas) {
this.on("change:dataoneQuotas", this.isAuthorizedCreatePortal);
return;
}
const portalQuotas = quotas.where({ quotaType: "portal" });
// If this user has no portal Quota at all, they are not auth to create a portal
if (!portalQuotas) {
this.set("isAuthorizedCreatePortal", false);
} else {
// Check that there is at least one Quota where the totalUsage < softLimit
const hasRemainingUsage = _.some(
portalQuotas,
(quota) => quota.get("totalUsage") < quota.get("softLimit"),
);
// If there is remaining usage left in at least one Quota, then the user can create a portal
if (hasRemainingUsage) {
this.set("isAuthorizedCreatePortal", true);
}
// Otherwise they cannot create a new portal
else {
this.set("isAuthorizedCreatePortal", false);
}
}
// @todoGet the admin group and force admins to have at least one quota left
} else {
// Default to letting people create portals
this.set("isAuthorizedCreatePortal", true);
}
},
/**
* Given a list of user and/or group subjects, this function checks if this user
* has an equivalent identity in that list, or is a member of a group in the list.
* A single subject string can be passed instead of an array of subjects.
* TODO: This needs to support nested group membership.
* @param {string|string[]} subjects
* @returns {boolean}
*/
hasIdentityOverlap(subjects) {
try {
// If only a single subject is given, put it in an array
if (typeof subjects === "string") {
subjects = [subjects];
}
// If the subjects are not a string or an array, or if it's an empty array, exit this function.
else if (!Array.isArray(subjects) || !subjects.length) {
return false;
}
return _.intersection(this.get("allIdentitiesAndGroups"), subjects)
.length;
} catch (e) {
console.error(e);
return false;
}
},
/**
* Retrieve all the info about this user's DataONE Subscription
*/
fetchSubscription() {
// If Bookkeeper services are disabled, exit
if (!MetacatUI.appModel.get("enableBookkeeperServices")) {
return;
}
try {
const thisUser = this;
require([
"collections/bookkeeper/Quotas",
"models/bookkeeper/Subscription",
], (Quotas, Subscription) => {
// Create a Quotas collection
const quotas = new Quotas();
// Create a Subscription model
const subscription = new Subscription();
if (MetacatUI.appModel.get("dataonePlusPreviewMode")) {
// Create Quota models for preview mode
quotas.add({
softLimit: MetacatUI.appModel.get("portalLimit"),
hardLimit: MetacatUI.appModel.get("portalLimit"),
quotaType: "portal",
unit: "portal",
subject: thisUser.get("username"),
subscription,
});
// Default to all people being in trial mode
subscription.set("status", "trialing");
// Save a reference to the Quotas on this UserModel
thisUser.set("dataoneQuotas", quotas);
// Save a reference to the Subscriptioin on this UserModel
thisUser.set("dataoneSubscription", subscription);
} else {
thisUser.listenToOnce(quotas, "reset", () => {
// Save a reference to the Quotas on this UserModel
thisUser.set("dataoneQuotas", quotas);
});
thisUser.listenToOnce(subscription, "sync", () => {
// Save a reference to the Subscriptioin on this UserModel
thisUser.set("dataoneSubscription", subscription);
});
// Fetch the Quotas
quotas.fetch({ subscriber: thisUser.get("username") });
// Fetch the Subscriptioin
subscription.fetch();
}
});
} catch (e) {
console.error(
"Couldn't get DataONE Subscription info. Proceeding as an unsubscribed user. ",
e,
);
}
},
/**
* Gets the already-fetched Quotas for the User, filters down to the type given, and returns them.
* @param {string} [type] - The Quota type to return
* @returns {Quota[]} The filtered array of Quota models or an empty array, if none are found
*/
getQuotas(type) {
const quotas = this.get("dataoneQuotas");
if (quotas && type) {
return quotas.where({ quotaType: type });
}
if (quotas && !type) {
return quotas;
}
return [];
},
reset() {
const defaults = _.omit(this.defaults(), [
"searchModel",
"searchResults",
]);
this.set(defaults);
},
},
);
return UserModel;
});