web-mojo
Version:
WEB-MOJO - A lightweight JavaScript framework for building data-driven web applications
368 lines (367 loc) • 10.8 kB
JavaScript
class Token {
constructor(token) {
this.token = token;
this.payload = null;
this.uid = null;
this.email = null;
this.name = null;
this.exp = null;
this.iat = null;
this.isValidToken = false;
this._decode();
}
/**
* Decode JWT token payload (client-side only, no verification)
* @private
*/
_decode() {
if (!this.token || typeof this.token !== "string") {
return;
}
try {
const parts = this.token.split(".");
if (parts.length !== 3) {
return;
}
const payload = parts[1];
let base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
const padding = 4 - base64.length % 4;
if (padding !== 4) {
base64 += "=".repeat(padding);
}
const decoded = atob(base64);
this.payload = JSON.parse(decoded);
this.uid = this.payload.uid || this.payload.sub || this.payload.user_id || null;
this.email = this.payload.email || null;
this.name = this.payload.name || this.payload.username || null;
this.exp = this.payload.exp ? new Date(this.payload.exp * 1e3) : null;
this.iat = this.payload.iat ? new Date(this.payload.iat * 1e3) : null;
this.isValidToken = this._checkValidity();
} catch (error) {
this.payload = null;
}
}
/**
* Check token validity
* @private
* @returns {boolean} True if token is valid
*/
_checkValidity() {
if (!this.token || !this.payload) {
return false;
}
if (this.payload.exp) {
const now = Math.floor(Date.now() / 1e3);
return now < this.payload.exp;
}
return true;
}
/**
* Decode JWT token payload (client-side only, no verification)
* @returns {object|null} Decoded payload or null if invalid
*/
decode() {
return this.payload;
}
/**
* Get user ID from token
* @returns {string|null} User ID or null if not found
*/
getUserId() {
return this.uid;
}
/**
* Check if token is valid (exists and not expired)
* @returns {boolean} True if token is valid
*/
isValid() {
return this.isValidToken;
}
/**
* Check if token will expire soon
* @param {number} thresholdMinutes - Minutes before expiry to consider "soon"
* @returns {boolean} True if expiring soon
*/
isExpiringSoon(thresholdMinutes = 5) {
if (!this.payload?.exp) {
return false;
}
const now = Math.floor(Date.now() / 1e3);
const threshold = thresholdMinutes * 60;
return this.payload.exp - now <= threshold;
}
/**
* Check if token is expired
* @returns {boolean} True if expired
*/
isExpired() {
if (!this.payload?.exp) {
return false;
}
const now = Math.floor(Date.now() / 1e3);
return now >= this.payload.exp;
}
/**
* Get token age in minutes
* @returns {number|null} Age in minutes since token was issued, or null if no iat
*/
getAgeMinutes() {
if (!this.payload?.iat) {
return null;
}
const now = Math.floor(Date.now() / 1e3);
const ageSeconds = now - this.payload.iat;
return Math.floor(ageSeconds / 60);
}
/**
* Get authorization header value
* @returns {string|null} Bearer token string or null if no token
*/
getAuthHeader() {
return this.token ? `Bearer ${this.token}` : null;
}
/**
* Get basic user info from token
* @returns {object|null} User info or null
*/
getUserInfo() {
if (!this.payload) {
return null;
}
return {
uid: this.uid,
email: this.email,
name: this.name,
exp: this.exp,
iat: this.iat
};
}
}
class TokenManager {
constructor() {
this.tokenKey = "mojo_auth_token";
this.refreshTokenKey = "mojo_auth_refresh_token";
this.tokenInstance = null;
}
/**
* Store authentication tokens
* @param {string} token - Access token
* @param {string} refreshToken - Refresh token (optional)
* @param {boolean} persistent - Use localStorage if true, sessionStorage if false
*/
setTokens(token, refreshToken = null, persistent = true) {
const storage = persistent ? localStorage : sessionStorage;
this.tokenInstance = new Token(token);
if (token) {
storage.setItem(this.tokenKey, token);
}
if (refreshToken) {
storage.setItem(this.refreshTokenKey, refreshToken);
}
}
/**
* Get stored access token
* @returns {string|null} Access token or null if not found
*/
getToken() {
return localStorage.getItem(this.tokenKey) || sessionStorage.getItem(this.tokenKey);
}
/**
* Get stored refresh token
* @returns {string|null} Refresh token or null if not found
*/
getRefreshToken() {
return localStorage.getItem(this.refreshTokenKey) || sessionStorage.getItem(this.refreshTokenKey);
}
/**
* Clear all stored tokens
*/
clearTokens() {
localStorage.removeItem(this.tokenKey);
localStorage.removeItem(this.refreshTokenKey);
sessionStorage.removeItem(this.tokenKey);
sessionStorage.removeItem(this.refreshTokenKey);
}
/**
* Get Token instance for current stored token
* @returns {Token|null} Token instance or null if no token
*/
getTokenInstance() {
const currentToken = this.getToken();
if (!currentToken) {
this.tokenInstance = null;
return null;
}
if (!this.tokenInstance || this.tokenInstance.token !== currentToken) {
this.tokenInstance = new Token(currentToken);
}
return this.tokenInstance;
}
/**
* Get Token instance for refresh token
* @returns {Token|null} Token instance or null if no refresh token
*/
getRefreshTokenInstance() {
const currentRefreshToken = this.getRefreshToken();
if (!currentRefreshToken) {
this._refreshTokenInstance = null;
return null;
}
if (!this._refreshTokenInstance || this._refreshTokenInstance.token !== currentRefreshToken) {
this._refreshTokenInstance = new Token(currentRefreshToken);
}
return this._refreshTokenInstance;
}
/**
* Decode JWT token payload (client-side only, no verification)
* @param {string} token - JWT token
* @returns {object|null} Decoded payload or null if invalid
*/
decode(token = null) {
const jwt = token || this.getToken();
return new Token(jwt).decode();
}
/**
* Get user ID from token
* @returns {string|null} User ID or null if not found
*/
getUserId() {
const currentToken = this.getTokenInstance();
return currentToken ? currentToken.getUserId() : null;
}
/**
* Check if current token is valid (exists and not expired)
* @returns {boolean} True if token is valid
*/
isValid() {
const currentToken = this.getTokenInstance();
return currentToken ? currentToken.isValid() : false;
}
/**
* Check if token will expire soon
* @param {number} thresholdMinutes - Minutes before expiry to consider "soon"
* @returns {boolean} True if expiring soon
*/
isExpiringSoon(thresholdMinutes = 5) {
const currentToken = this.getTokenInstance();
return currentToken ? currentToken.isExpiringSoon(thresholdMinutes) : false;
}
/**
* Get authorization header value
* @returns {string|null} Bearer token string or null if no token
*/
getAuthHeader() {
const currentToken = this.getTokenInstance();
return currentToken ? currentToken.getAuthHeader() : null;
}
/**
* Get basic user info from token
* @returns {object|null} User info or null
*/
getUserInfo() {
const currentToken = this.getTokenInstance();
return currentToken ? currentToken.getUserInfo() : null;
}
/**
* Check current token status and determine what action is needed
* @returns {object} Status object with action and details
*/
checkTokenStatus() {
const token = this.getTokenInstance();
const refreshToken = this.getRefreshTokenInstance();
if (!token || !token.isValid() || token.isExpired()) {
if (!refreshToken || !refreshToken.isValid() || refreshToken.isExpired()) {
return {
action: "logout",
reason: "Both access and refresh tokens are invalid/expired"
};
}
return {
action: "refresh",
reason: "Access token invalid/expired but refresh token valid"
};
}
if (token.isExpiringSoon(10) || token.getAgeMinutes() && token.getAgeMinutes() > 60) {
if (!refreshToken || !refreshToken.isValid() || refreshToken.isExpired()) {
return {
action: "none",
reason: "Access token expiring but refresh token invalid"
};
}
return {
action: "refresh",
reason: "Access token expiring soon or aged"
};
}
return {
action: "none",
reason: "All tokens valid and not expiring soon"
};
}
/**
* Check tokens and take appropriate action
* @param {object} app - App instance for events and API calls
* @returns {Promise<boolean>} True if action was taken
*/
async checkAndRefreshTokens(app) {
const status = this.checkTokenStatus();
switch (status.action) {
case "logout":
app.events.emit("auth:unauthorized");
this.stopAutoRefresh();
return true;
case "refresh":
await this.refreshToken(app);
return true;
default:
return false;
}
}
startAutoRefresh(app) {
this.stopAutoRefresh();
this._tokenWatcher = setInterval(() => {
this.checkAndRefreshTokens(app);
}, 6e4);
}
stopAutoRefresh() {
if (this._tokenWatcher) {
clearInterval(this._tokenWatcher);
this._tokenWatcher = null;
}
}
async refreshToken(app) {
const refreshTokenInstance = this.getRefreshTokenInstance();
if (!refreshTokenInstance || !refreshTokenInstance.isValid() || refreshTokenInstance.isExpired()) {
app.events.emit("auth:unauthorized");
this.stopAutoRefresh();
return;
}
try {
const response = await app.rest.POST("/api/token/refresh", {
refresh_token: refreshTokenInstance.token
});
const { access_token, refresh_token } = response.data.data;
this.tokenInstance = null;
this._refreshTokenInstance = null;
this.setTokens(access_token, refresh_token);
app.rest.setAuthToken(access_token);
app.events.emit("auth:token:refreshed", {
newToken: access_token,
newRefreshToken: refresh_token
});
console.log("Token refreshed successfully");
} catch (error) {
if (error.status === 401 || error.status === 403) {
app.events.emit("auth:unauthorized");
this.stopAutoRefresh();
} else {
app.events.emit("auth:token:refresh:failed", { error });
app.showError(`Token refresh failed: ${error.message}`);
}
}
}
}
export {
TokenManager as T
};
//# sourceMappingURL=TokenManager-5HHjYzTo.js.map