mirror of
https://github.com/rr-/szurubooru.git
synced 2025-07-17 08:26:24 +00:00
Initial calls to api.fetchConfig() in quick succession would launch multiple requests. This should be implemented in Info tbh not outside of it.
476 lines
14 KiB
JavaScript
476 lines
14 KiB
JavaScript
"use strict";
|
|
|
|
const cookies = require("js-cookie");
|
|
const request = require("superagent");
|
|
const events = require("./events.js");
|
|
const progress = require("./util/progress.js");
|
|
const uri = require("./util/uri.js");
|
|
const Info = require("../models/info.js");
|
|
|
|
let fileTokens = {};
|
|
let remoteConfig = null;
|
|
let remoteConfigPromise = null;
|
|
|
|
class Api extends events.EventTarget {
|
|
constructor() {
|
|
super();
|
|
this.user = null;
|
|
this.userName = null;
|
|
this.userPassword = null;
|
|
this.token = null;
|
|
this.cache = {};
|
|
this.allRanks = [
|
|
"anonymous",
|
|
"restricted",
|
|
"regular",
|
|
"power",
|
|
"moderator",
|
|
"administrator",
|
|
"nobody",
|
|
];
|
|
this.rankNames = new Map([
|
|
["anonymous", "Anonymous"],
|
|
["restricted", "Restricted user"],
|
|
["regular", "Regular user"],
|
|
["power", "Power user"],
|
|
["moderator", "Moderator"],
|
|
["administrator", "Administrator"],
|
|
["nobody", "Nobody"],
|
|
]);
|
|
}
|
|
|
|
get(url, options) {
|
|
if (url in this.cache) {
|
|
return new Promise((resolve, reject) => {
|
|
resolve(this.cache[url]);
|
|
});
|
|
}
|
|
return this._wrappedRequest(url, request.get, {}, {}, options).then(
|
|
(response) => {
|
|
this.cache[url] = response;
|
|
return Promise.resolve(response);
|
|
}
|
|
);
|
|
}
|
|
|
|
post(url, data, files, options) {
|
|
this.cache = {};
|
|
return this._wrappedRequest(url, request.post, data, files, options);
|
|
}
|
|
|
|
put(url, data, files, options) {
|
|
this.cache = {};
|
|
return this._wrappedRequest(url, request.put, data, files, options);
|
|
}
|
|
|
|
delete(url, data, options) {
|
|
this.cache = {};
|
|
return this._wrappedRequest(url, request.delete, data, {}, options);
|
|
}
|
|
|
|
fetchConfig() {
|
|
if (remoteConfig === null) {
|
|
if (remoteConfigPromise !== null) {
|
|
return Promise.resolve(remoteConfigPromise);
|
|
}
|
|
remoteConfigPromise = Info.get().then((response) => {
|
|
remoteConfig = response.config;
|
|
});
|
|
return remoteConfigPromise;
|
|
} else {
|
|
return Promise.resolve();
|
|
}
|
|
}
|
|
|
|
getName() {
|
|
return remoteConfig.name;
|
|
}
|
|
|
|
getTagNameRegex() {
|
|
return remoteConfig.tagNameRegex;
|
|
}
|
|
|
|
getPoolNameRegex() {
|
|
return remoteConfig.poolNameRegex;
|
|
}
|
|
|
|
getPasswordRegex() {
|
|
return remoteConfig.passwordRegex;
|
|
}
|
|
|
|
getUserNameRegex() {
|
|
return remoteConfig.userNameRegex;
|
|
}
|
|
|
|
getContactEmail() {
|
|
return remoteConfig.contactEmail;
|
|
}
|
|
|
|
canSendMails() {
|
|
return !!remoteConfig.canSendMails;
|
|
}
|
|
|
|
safetyEnabled() {
|
|
return !!remoteConfig.enableSafety;
|
|
}
|
|
|
|
hasPrivilege(lookup) {
|
|
let minViableRank = null;
|
|
for (let p of Object.keys(remoteConfig.privileges)) {
|
|
if (!p.startsWith(lookup)) {
|
|
continue;
|
|
}
|
|
const rankIndex = this.allRanks.indexOf(
|
|
remoteConfig.privileges[p]
|
|
);
|
|
if (minViableRank === null || rankIndex < minViableRank) {
|
|
minViableRank = rankIndex;
|
|
}
|
|
}
|
|
if (minViableRank === null) {
|
|
throw `Bad privilege name: ${lookup}`;
|
|
}
|
|
let myRank =
|
|
this.user !== null ? this.allRanks.indexOf(this.user.rank) : 0;
|
|
return myRank >= minViableRank;
|
|
}
|
|
|
|
loginFromCookies() {
|
|
const auth = cookies.getJSON("auth");
|
|
return auth && auth.user && auth.token
|
|
? this.loginWithToken(auth.user, auth.token, true)
|
|
: Promise.resolve();
|
|
}
|
|
|
|
loginWithToken(userName, token, doRemember) {
|
|
this.cache = {};
|
|
return new Promise((resolve, reject) => {
|
|
this.userName = userName;
|
|
this.token = token;
|
|
this.get("/user/" + userName + "?bump-login=true").then(
|
|
(response) => {
|
|
const options = {};
|
|
if (doRemember) {
|
|
options.expires = 365;
|
|
}
|
|
cookies.set(
|
|
"auth",
|
|
{ user: userName, token: token },
|
|
options
|
|
);
|
|
this.user = response;
|
|
resolve();
|
|
this.dispatchEvent(new CustomEvent("login"));
|
|
},
|
|
(error) => {
|
|
reject(error);
|
|
this.logout();
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
createToken(userName, options) {
|
|
let userTokenRequest = {
|
|
enabled: true,
|
|
note: "Web Login Token",
|
|
};
|
|
if (typeof options.expires !== "undefined") {
|
|
userTokenRequest.expirationTime = new Date()
|
|
.addDays(options.expires)
|
|
.toISOString();
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
this.post("/user-token/" + userName, userTokenRequest).then(
|
|
(response) => {
|
|
cookies.set(
|
|
"auth",
|
|
{ user: userName, token: response.token },
|
|
options
|
|
);
|
|
this.userName = userName;
|
|
this.token = response.token;
|
|
this.userPassword = null;
|
|
},
|
|
(error) => {
|
|
reject(error);
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
deleteToken(userName, userToken) {
|
|
return new Promise((resolve, reject) => {
|
|
this.delete("/user-token/" + userName + "/" + userToken, {}).then(
|
|
(response) => {
|
|
const options = {};
|
|
cookies.set(
|
|
"auth",
|
|
{ user: userName, token: null },
|
|
options
|
|
);
|
|
resolve();
|
|
},
|
|
(error) => {
|
|
reject(error);
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
login(userName, userPassword, doRemember) {
|
|
this.cache = {};
|
|
return new Promise((resolve, reject) => {
|
|
this.userName = userName;
|
|
this.userPassword = userPassword;
|
|
this.get("/user/" + userName + "?bump-login=true").then(
|
|
(response) => {
|
|
const options = {};
|
|
if (doRemember) {
|
|
options.expires = 365;
|
|
}
|
|
this.createToken(this.userName, options);
|
|
this.user = response;
|
|
resolve();
|
|
this.dispatchEvent(new CustomEvent("login"));
|
|
},
|
|
(error) => {
|
|
reject(error);
|
|
this.logout();
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
logout() {
|
|
let self = this;
|
|
this.deleteToken(this.userName, this.token).then(
|
|
(response) => {
|
|
self._logout();
|
|
},
|
|
(error) => {
|
|
self._logout();
|
|
}
|
|
);
|
|
}
|
|
|
|
_logout() {
|
|
this.user = null;
|
|
this.userName = null;
|
|
this.userPassword = null;
|
|
this.token = null;
|
|
this.dispatchEvent(new CustomEvent("logout"));
|
|
}
|
|
|
|
forget() {
|
|
cookies.remove("auth");
|
|
}
|
|
|
|
isLoggedIn(user) {
|
|
if (user) {
|
|
return (
|
|
this.userName !== null &&
|
|
this.userName.toLowerCase() === user.name.toLowerCase()
|
|
);
|
|
} else {
|
|
return this.userName !== null;
|
|
}
|
|
}
|
|
|
|
isCurrentAuthToken(userToken) {
|
|
return userToken.token === this.token;
|
|
}
|
|
|
|
_getFullUrl(url) {
|
|
const fullUrl = ("api/" + url).replace(/([^:])\/+/g, "$1/");
|
|
const matches = fullUrl.match(/^([^?]*)\??(.*)$/);
|
|
const baseUrl = matches[1];
|
|
const request = matches[2];
|
|
return [baseUrl, request];
|
|
}
|
|
|
|
_getFileId(file) {
|
|
if (file.constructor === String) {
|
|
return file;
|
|
}
|
|
return file.name + file.size;
|
|
}
|
|
|
|
_wrappedRequest(url, requestFactory, data, files, options) {
|
|
// transform the request: upload each file, then make the request use
|
|
// its tokens.
|
|
data = Object.assign({}, data);
|
|
let abortFunction = () => {};
|
|
let promise = Promise.resolve();
|
|
if (files) {
|
|
for (let key of Object.keys(files)) {
|
|
const file = files[key];
|
|
const fileId = this._getFileId(file);
|
|
if (fileTokens[fileId]) {
|
|
data[key + "Token"] = fileTokens[fileId];
|
|
} else {
|
|
promise = promise
|
|
.then(() => {
|
|
let uploadPromise = this._upload(file);
|
|
abortFunction = () => uploadPromise.abort();
|
|
return uploadPromise;
|
|
})
|
|
.then((token) => {
|
|
abortFunction = () => {};
|
|
fileTokens[fileId] = token;
|
|
data[key + "Token"] = token;
|
|
return Promise.resolve();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
promise = promise
|
|
.then(() => {
|
|
let requestPromise = this._rawRequest(
|
|
url,
|
|
requestFactory,
|
|
data,
|
|
{},
|
|
options
|
|
);
|
|
abortFunction = () => requestPromise.abort();
|
|
return requestPromise;
|
|
})
|
|
.catch((error) => {
|
|
if (
|
|
error.response &&
|
|
error.response.name === "MissingOrExpiredRequiredFileError"
|
|
) {
|
|
for (let key of Object.keys(files)) {
|
|
const file = files[key];
|
|
const fileId = this._getFileId(file);
|
|
fileTokens[fileId] = null;
|
|
}
|
|
error.message =
|
|
"The uploaded file has expired; " +
|
|
"please resend the form to reupload.";
|
|
}
|
|
return Promise.reject(error);
|
|
});
|
|
promise.abort = () => abortFunction();
|
|
return promise;
|
|
}
|
|
|
|
_upload(file, options) {
|
|
let abortFunction = () => {};
|
|
let returnedPromise = new Promise((resolve, reject) => {
|
|
let uploadPromise = this._rawRequest(
|
|
"uploads",
|
|
request.post,
|
|
{},
|
|
{ content: file },
|
|
options
|
|
);
|
|
abortFunction = () => uploadPromise.abort();
|
|
return uploadPromise.then((response) => {
|
|
abortFunction = () => {};
|
|
return resolve(response.token);
|
|
}, reject);
|
|
});
|
|
returnedPromise.abort = () => abortFunction();
|
|
return returnedPromise;
|
|
}
|
|
|
|
_rawRequest(url, requestFactory, data, files, options) {
|
|
options = options || {};
|
|
data = Object.assign({}, data);
|
|
const [fullUrl, query] = this._getFullUrl(url);
|
|
|
|
let abortFunction = () => {};
|
|
let returnedPromise = new Promise((resolve, reject) => {
|
|
let req = requestFactory(fullUrl);
|
|
|
|
req.set("Accept", "application/json");
|
|
|
|
if (query) {
|
|
req.query(query);
|
|
}
|
|
|
|
if (files) {
|
|
for (let key of Object.keys(files)) {
|
|
const value = files[key];
|
|
if (value.constructor === String) {
|
|
data[key + "Url"] = value;
|
|
} else {
|
|
req.attach(key, value || new Blob());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (data) {
|
|
if (files && Object.keys(files).length) {
|
|
req.attach("metadata", new Blob([JSON.stringify(data)], { type: "application/json" }));
|
|
} else {
|
|
req.set("Content-Type", "application/json");
|
|
req.send(data);
|
|
}
|
|
}
|
|
|
|
try {
|
|
if (this.userName && this.token) {
|
|
req.auth = null;
|
|
// eslint-disable-next-line no-undef
|
|
req.set(
|
|
"Authorization",
|
|
"Token " +
|
|
new Buffer(
|
|
this.userName + ":" + this.token
|
|
).toString("base64")
|
|
);
|
|
} else if (this.userName && this.userPassword) {
|
|
req.auth(
|
|
this.userName,
|
|
encodeURIComponent(this.userPassword).replace(
|
|
/%([0-9A-F]{2})/g,
|
|
(match, p1) => {
|
|
return String.fromCharCode("0x" + p1);
|
|
}
|
|
)
|
|
);
|
|
}
|
|
} catch (e) {
|
|
reject(
|
|
new Error("Authentication error (malformed credentials)")
|
|
);
|
|
}
|
|
|
|
if (!options.noProgress) {
|
|
progress.start();
|
|
}
|
|
|
|
abortFunction = () => {
|
|
req.abort(); // does *NOT* call the callback passed in .end()
|
|
progress.done();
|
|
reject(
|
|
new Error("The request was aborted due to user cancel.")
|
|
);
|
|
};
|
|
|
|
req.end((error, response) => {
|
|
progress.done();
|
|
abortFunction = () => {};
|
|
if (error) {
|
|
if (response && response.body) {
|
|
error = new Error(
|
|
response.body.description || "Unknown error"
|
|
);
|
|
error.response = response.body;
|
|
}
|
|
reject(error);
|
|
} else {
|
|
resolve(response.body);
|
|
}
|
|
});
|
|
});
|
|
returnedPromise.abort = () => abortFunction();
|
|
return returnedPromise;
|
|
}
|
|
}
|
|
|
|
module.exports = new Api();
|