Skip to content

Commit

Permalink
[feature] Added option to show user's data usage on status page #735
Browse files Browse the repository at this point in the history
When subscriptions module is used, show user's current plan.
If user's current plan is free, then show option to upgrade the plan.

Closes #735
  • Loading branch information
pandafy committed Dec 19, 2023
1 parent d6698f3 commit 793cb7e
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 3 deletions.
46 changes: 46 additions & 0 deletions client/components/status/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,52 @@
overflow-wrap: break-word;
word-wrap: break-word;
}
.flex-wrapper {
flex-wrap: wrap;
max-width: 1000px;
margin: 0 auto;
}

.flex-row {
flex-grow: 1;
flex-direction: column;
flex-wrap: wrap;
width: 100%;
}

.flex-row > .row {
padding: 3%;
margin: 0 0 10px;
box-sizing: border-box;
}

.limit-info {
text-align: center;
margin-bottom: 15px;
}

.limit-info h3 {
margin: 0 0 10px;
}
.limit-info progress {
width: 80%;
height: 40px;
}
.limit-info p.progress {
font-size: 20px;
}
.limit-info p:last-child {
margin-bottom: 0;
}
.limit-info .button {
margin-top: 5px;
}

.limit-info .exahusted {
color: #ba2121f7;
margin-top: 15px;
line-height: 1.5em;
}

@media screen and (min-width: 0px) and (max-width: 500px) {
.logout-modal-container p.message {
Expand Down
101 changes: 99 additions & 2 deletions client/components/status/status.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ import {Link} from "react-router-dom";
import {toast} from "react-toastify";
import InfinteScroll from "react-infinite-scroll-component";
import {t, gettext} from "ttag";
import {getUserRadiusSessionsUrl, mainToastId} from "../../constants";
import prettyBytes from "pretty-bytes";
import {timeFromSeconds} from "duration-formatter";
import {
getUserRadiusSessionsUrl,
getUserRadiusUsageUrl,
mainToastId,
} from "../../constants";
import LoadingContext from "../../utils/loading-context";
import getText from "../../utils/get-text";
import logError from "../../utils/log-error";
Expand Down Expand Up @@ -48,6 +54,8 @@ export default class Status extends React.Component {
loadSpinner: true,
modalActive: false,
rememberMe: false,
userChecks: [],
userPlan: {},
};
this.repeatLogin = false;
this.getUserRadiusSessions = this.getUserRadiusSessions.bind(this);
Expand Down Expand Up @@ -196,6 +204,7 @@ export default class Status extends React.Component {

componentWillUnmount = () => {
clearInterval(this.intervalId);
clearInterval(this.getUserRadiusUsageIntervalId);
window.removeEventListener("resize", this.updateScreenWidth);
};

Expand Down Expand Up @@ -238,6 +247,10 @@ export default class Status extends React.Component {
this.intervalId = setInterval(() => {
this.getUserActiveRadiusSessions();
}, 60000);
await this.getUserRadiusUsage();
this.getUserRadiusUsageIntervalId = setInterval(() => {
this.getUserRadiusUsage();
}, 60000);
window.addEventListener("resize", this.updateScreenWidth);
this.updateSpinner();
}
Expand Down Expand Up @@ -288,6 +301,41 @@ export default class Status extends React.Component {
}
}

async getUserRadiusUsage() {
const {cookies, orgSlug, logout, userData} = this.props;
const url = getUserRadiusUsageUrl(orgSlug);
const auth_token = cookies.get(`${orgSlug}_auth_token`);
handleSession(orgSlug, auth_token, cookies);
const options = {};
try {
const response = await axios({
method: "get",
headers: {
"content-type": "application/x-www-form-urlencoded",
Authorization: `Bearer ${userData.auth_token}`,
},
url,
});
options.userChecks = response.data.checks;
if (response.data.plan) {
options.userPlan = response.data.plan;
}
this.setState(options);
} catch (error) {
// logout only if unauthorized or forbidden
if (
error.response &&
(error.response.status === 401 || error.response.status === 403)
) {
logout(cookies, orgSlug);
toast.error(t`ERR_OCCUR`, {
onOpen: () => toast.dismiss(mainToastId),
});
}
logError(error, t`ERR_OCCUR`);
}
}

async getUserActiveRadiusSessions(params = {}) {
const para = {
is_open: true,
Expand Down Expand Up @@ -752,6 +800,17 @@ export default class Status extends React.Component {
},
});

getUserCheckFormattedValue = (value, type) => {
switch (type) {
case "bytes":
return prettyBytes(parseInt(value, 10));
case "seconds":
return timeFromSeconds(parseInt(value, 10));
default:
return value;
}
};

render() {
const {
statusPage,
Expand All @@ -762,13 +821,16 @@ export default class Status extends React.Component {
isAuthenticated,
userData,
internetMode,
settings,
} = this.props;
const {links} = statusPage;
const {
username,
password,
userInfo,
activeSessions,
userChecks,
userPlan,
pastSessions,
sessionsToLogout,
hasMoreSessions,
Expand All @@ -787,7 +849,42 @@ export default class Status extends React.Component {
handleResponse={this.handleLogout}
content={<p className="message">{t`LOGOUT_MODAL_CONTENT`}</p>}
/>
<div className="container content" id="status">
<div className="container content flex-wrapper" id="status">
<div className="inner flex-row limit-info">
<div className="bg row">
{settings.subscriptions && (
<h3>Current subscriptions: {userPlan.name}</h3>
)}
{userChecks &&
userChecks.map((check) => (
<div>
<progress
id={check.attribute}
max={check.value}
value={check.result}
/>
<p className="progress">
<strong>
{this.getUserCheckFormattedValue(
check.result,
check.type,
)}
</strong>{" "}
of{" "}
{this.getUserCheckFormattedValue(check.value, check.type)}{" "}
used
</p>
</div>
))}
{settings.subscriptions && userPlan.is_free && (
<p>
<button type="button" className="button partial">
Upgrade
</button>
</p>
)}
</div>
</div>
<div className="inner">
<div className="main-column">
<div className="inner">
Expand Down
2 changes: 2 additions & 0 deletions client/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export const paymentStatusUrl = (orgSlug, paymentId) =>
`${prefix}/${orgSlug}/payment/status/${paymentId}`;
export const getUserRadiusSessionsUrl = (orgSlug) =>
`${prefix}/${orgSlug}/account/session`;
export const getUserRadiusUsageUrl = (orgSlug) =>
`${prefix}/${orgSlug}/account/usage`;
export const createMobilePhoneTokenUrl = (orgSlug) =>
`${prefix}/${orgSlug}/account/phone/token`;
export const mobilePhoneTokenStatusUrl = (orgSlug) =>
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"dependencies": {
"@babel/register": "^7.10.1",
"@types/morgan": "^1.9.2",
"add": "^2.0.6",
"autoprefixer": "^9.8.0",
"axios": "^0.24.0",
"compression": "^1.7.4",
Expand All @@ -16,6 +17,7 @@
"deep-object-diff": "^1.1.0",
"deepmerge": "^4.2.2",
"dompurify": "^3.0.6",
"duration-formatter": "^1.0.7",
"express": "^4.17.1",
"fs-extra": "^11.1.0",
"history": "^5.2.0",
Expand All @@ -27,6 +29,7 @@
"morgan": "^1.10.0",
"node-plop": "^0.31.0",
"nodemon": "^2.0.4",
"pretty-bytes": "^6.1.1",
"prop-types": "^15.7.2",
"qs": "^6.9.4",
"raf": "^3.4.1",
Expand Down
70 changes: 70 additions & 0 deletions server/controllers/user-radius-usage-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import axios from "axios";
import merge from "deepmerge";
import config from "../config.json";
import defaultConfig from "../utils/default-config";
import {logResponseError} from "../utils/logger";
import reverse from "../utils/openwisp-urls";
import getSlug from "../utils/get-slug";

const getUserRadiusUsage = (req, res) => {
const reqOrg = req.params.organization;
const validSlug = config.some((org) => {
if (org.slug === reqOrg) {
// merge default config and custom config
const conf = merge(defaultConfig, org);
const {host} = conf;
let radiusUsagePathName;
if (conf.settings.subscriptions) {
radiusUsagePathName = "user_plan_radius_usage";
} else {
radiusUsagePathName = "user_radius_usage";
}
const userRadiusUsageUrl = reverse(radiusUsagePathName, getSlug(conf));
const timeout = conf.timeout * 1000;
// make AJAX request
axios({
method: "get",
headers: {
"content-type": "application/x-www-form-urlencoded",
Authorization: req.headers.authorization,
"accept-language": req.headers["accept-language"],
},
url: `${host}${userRadiusUsageUrl}/`,
timeout,
params: req.query,
})
.then((response) => {
if ("link" in response.headers) {
res.setHeader("link", response.headers.link);
}
res
.status(response.status)
.type("application/json")
.send(response.data);
})
.catch((error) => {
logResponseError(error);
// forward error
try {
res
.status(error.response.status)
.type("application/json")
.send(error.response.data);
} catch (err) {
res.status(500).type("application/json").send({
response_code: "INTERNAL_SERVER_ERROR",
});
}
});
}
return org.slug === reqOrg;
});
// return 404 for invalid organization slug or org not listed in config
if (!validSlug) {
res.status(404).type("application/json").send({
response_code: "INTERNAL_SERVER_ERROR",
});
}
};

export default getUserRadiusUsage;
2 changes: 2 additions & 0 deletions server/routes/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import passwordResetConfirm from "../controllers/password-reset-confirm-controll
import passwordReset from "../controllers/password-reset-controller";
import registration from "../controllers/registration-controller";
import getUserRadiusSessions from "../controllers/user-radius-sessions-controller";
import getUserRadiusUsage from "../controllers/user-radius-usage-controller";
import validateToken from "../controllers/validate-token-controller";
import {
createMobilePhoneToken,
Expand All @@ -23,6 +24,7 @@ router.post("/password/reset/confirm/", errorHandler(passwordResetConfirm));
router.post("/password/reset", errorHandler(passwordReset));
router.post("/", errorHandler(registration));
router.get("/session/", errorHandler(getUserRadiusSessions));
router.get("/usage/", errorHandler(getUserRadiusUsage));
router.post("/phone/token", errorHandler(createMobilePhoneToken));
router.get("/phone/token/status", errorHandler(mobilePhoneTokenStatus));
router.post("/phone/verify", errorHandler(verifyMobilePhoneToken));
Expand Down
8 changes: 7 additions & 1 deletion server/utils/openwisp-urls.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const paths = {
user_auth_token: "/account/token",
validate_auth_token: "/account/token/validate",
user_radius_sessions: "/account/session",
user_radius_usage: "/account/usage",
user_plan_radius_usage: "/account/plan",
create_mobile_phone_token: "/account/phone/token",
mobile_phone_token_status: "/account/phone/token/active",
verify_mobile_phone_token: "/account/phone/verify",
Expand All @@ -21,7 +23,11 @@ const reverse = (name, orgSlug) => {
if (!path) {
throw new Error(`Reverse for path "${name}" not found.`);
}
if (name === "plans" || name === "payment_status") {
if (
name === "plans" ||
name === "payment_status" ||
name === "user_plan_radius_usage"
) {
prefix = prefix.replace("/radius/", "/subscriptions/");
}
return `${prefix.replace("{orgSlug}", orgSlug)}${path}`;
Expand Down
15 changes: 15 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2573,6 +2573,11 @@ acorn@^8.0.4, acorn@^8.2.4:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf"
integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==

add@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/add/-/add-2.0.6.tgz#248f0a9f6e5a528ef2295dbeec30532130ae2235"
integrity sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q==

agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
Expand Down Expand Up @@ -4996,6 +5001,11 @@ duplexify@^3.4.2, duplexify@^3.6.0:
readable-stream "^2.0.0"
stream-shift "^1.0.0"

duration-formatter@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/duration-formatter/-/duration-formatter-1.0.7.tgz#caf764f2f7911ed629b0e19a2f0a9bf30422555d"
integrity sha512-uM0qX7L1HF1m3zX4yPYU264chtwZUmneHUmzEYG70pTO+PTjhSeMm7v6L+q+ZFOFerT39Gce6WXX+FMssBWplQ==

ecc-jsbn@~0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
Expand Down Expand Up @@ -10110,6 +10120,11 @@ prettier@^2.2.0:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.2.tgz#e26d71a18a74c3d0f0597f55f01fb6c06c206032"
integrity sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==

pretty-bytes@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-6.1.1.tgz#38cd6bb46f47afbf667c202cfc754bffd2016a3b"
integrity sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==

pretty-error@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.2.tgz#be89f82d81b1c86ec8fdfbc385045882727f93b6"
Expand Down

0 comments on commit 793cb7e

Please sign in to comment.