learnworlds-js
Version:
TypeScript SDK for LearnWorlds API with full type safety and comprehensive method coverage
325 lines (322 loc) • 9.86 kB
JavaScript
// src/client.ts
import axios2 from "axios";
// src/oauth.ts
import axios from "axios";
var OAuth2Client = class {
http;
config;
accessToken;
refreshToken;
tokenExpiresAt;
constructor(config) {
this.config = config;
this.accessToken = config.accessToken;
this.refreshToken = config.refreshToken;
const baseURL = `https://${config.schoolDomain}.learnworlds.com`;
this.http = axios.create({
baseURL,
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
});
}
/**
* Get the authorization URL for the authorization code grant flow
*/
getAuthorizationUrl(scope = "read_user_profile", state) {
const params = new URLSearchParams({
client_id: this.config.clientId,
redirect_uri: this.config.redirectUri || "",
response_type: "code",
scope
});
if (state) {
params.append("state", state);
}
return `https://${this.config.schoolDomain}.learnworlds.com/oauth2/authorize?${params.toString()}`;
}
/**
* Exchange authorization code for access token
*/
async exchangeAuthorizationCode(request) {
const params = new URLSearchParams({
grant_type: "authorization_code",
code: request.code,
redirect_uri: request.redirectUri,
client_id: this.config.clientId,
client_secret: this.config.clientSecret
});
const response = await this.http.post("/oauth2/token", params.toString());
await this.handleTokenResponse(response.data);
return response.data;
}
/**
* Get access token using resource owner password credentials grant
*/
async authenticateWithPassword(request) {
const params = new URLSearchParams({
grant_type: "password",
username: request.username,
password: request.password,
client_id: this.config.clientId,
client_secret: this.config.clientSecret
});
if (request.scope) {
params.append("scope", request.scope);
}
const response = await this.http.post("/oauth2/token", params.toString());
await this.handleTokenResponse(response.data);
return response.data;
}
/**
* Get access token using client credentials grant
* This is typically used for server-to-server authentication
*/
async authenticateWithClientCredentials(scope) {
const params = new URLSearchParams({
grant_type: "client_credentials",
client_id: this.config.clientId,
client_secret: this.config.clientSecret
});
if (scope) {
params.append("scope", scope);
}
const response = await this.http.post("/oauth2/token", params.toString());
await this.handleTokenResponse(response.data);
return response.data;
}
/**
* Refresh the access token using the refresh token
*/
async refreshAccessToken() {
if (!this.refreshToken) {
throw new Error("No refresh token available");
}
const params = new URLSearchParams({
grant_type: "refresh_token",
refresh_token: this.refreshToken,
client_id: this.config.clientId,
client_secret: this.config.clientSecret
});
const response = await this.http.post("/oauth2/token", params.toString());
await this.handleTokenResponse(response.data);
return response.data;
}
/**
* Revoke the access token
*/
async revokeToken(token) {
const tokenToRevoke = token || this.accessToken;
if (!tokenToRevoke) {
throw new Error("No token to revoke");
}
const params = new URLSearchParams({
token: tokenToRevoke,
client_id: this.config.clientId,
client_secret: this.config.clientSecret
});
await this.http.post("/oauth2/revoke", params.toString());
if (!token || token === this.accessToken) {
this.accessToken = void 0;
this.tokenExpiresAt = void 0;
}
}
/**
* Get the current access token, refreshing if necessary
*/
async getAccessToken() {
if (!this.accessToken) {
throw new Error("No access token available. Please authenticate first.");
}
if (this.tokenExpiresAt && this.refreshToken) {
const now = /* @__PURE__ */ new Date();
const bufferTime = 5 * 60 * 1e3;
if (now.getTime() >= this.tokenExpiresAt.getTime() - bufferTime) {
await this.refreshAccessToken();
}
}
return this.accessToken;
}
/**
* Check if authenticated
*/
isAuthenticated() {
return !!this.accessToken;
}
/**
* Set tokens manually (useful for restoring session)
*/
setTokens(tokens) {
this.accessToken = tokens.accessToken;
this.refreshToken = tokens.refreshToken;
this.tokenExpiresAt = tokens.expiresAt;
}
/**
* Get current tokens (useful for persisting session)
*/
getTokens() {
return {
accessToken: this.accessToken,
refreshToken: this.refreshToken,
expiresAt: this.tokenExpiresAt
};
}
async handleTokenResponse(response) {
this.accessToken = response.access_token;
if (response.refresh_token) {
this.refreshToken = response.refresh_token;
}
if (response.expires_in) {
const expiresAt = /* @__PURE__ */ new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + response.expires_in);
this.tokenExpiresAt = expiresAt;
}
if (this.config.onTokenRefresh) {
await this.config.onTokenRefresh(response);
}
}
};
// src/client.ts
var LearnWorldsClient = class {
http;
oauth;
constructor(config) {
this.oauth = new OAuth2Client(config);
const baseURL = `https://${config.apiHost}/v2`;
this.http = axios2.create({
baseURL,
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
}
});
this.http.interceptors.request.use(
async (config2) => {
try {
const token = await this.oauth.getAccessToken();
config2.headers.Authorization = `Bearer ${token}`;
} catch (error) {
}
return config2;
},
(error) => Promise.reject(error)
);
this.http.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry && this.oauth.getTokens().refreshToken) {
originalRequest._retry = true;
try {
await this.oauth.refreshAccessToken();
const token = await this.oauth.getAccessToken();
originalRequest.headers = originalRequest.headers || {};
originalRequest.headers.Authorization = `Bearer ${token}`;
return this.http(originalRequest);
} catch (refreshError) {
return Promise.reject(this.handleError(error));
}
}
return Promise.reject(this.handleError(error));
}
);
}
/**
* Get the OAuth2 client for authentication operations
*/
get auth() {
return this.oauth;
}
handleError(error) {
const lwError = new Error();
lwError.name = "LearnWorldsError";
if (error.response) {
lwError.status = error.response.status;
lwError.message = error.response.data?.message || error.message || "API Error";
lwError.details = error.response.data;
switch (error.response.status) {
case 401:
lwError.code = "UNAUTHORIZED";
lwError.message = "Invalid API key or authentication failed";
break;
case 403:
lwError.code = "FORBIDDEN";
lwError.message = "Access denied";
break;
case 404:
lwError.code = "NOT_FOUND";
lwError.message = "Resource not found";
break;
case 422:
lwError.code = "VALIDATION_ERROR";
lwError.message = "Validation failed";
break;
case 429:
lwError.code = "RATE_LIMIT";
lwError.message = "Rate limit exceeded";
break;
default:
lwError.code = "API_ERROR";
}
} else if (error.request) {
lwError.code = "NETWORK_ERROR";
lwError.message = "Network error - unable to reach API";
} else {
lwError.code = "UNKNOWN_ERROR";
lwError.message = error.message || "Unknown error occurred";
}
return lwError;
}
async makeRequest(method, endpoint, data, params) {
const response = await this.http.request({
method,
url: endpoint,
data,
params
});
if (!response.data.success) {
const error = new Error(response.data.message || "API request failed");
error.code = "API_ERROR";
error.details = response.data.errors;
throw error;
}
return response.data.data;
}
async getAllCourses(params) {
return this.makeRequest("GET", "/courses", void 0, params);
}
async getAllBundles(params) {
return this.makeRequest("GET", "/bundles", void 0, params);
}
async createUser(userData) {
return this.makeRequest("POST", "/users", userData);
}
async updateUser(userId, userData) {
return this.makeRequest("PUT", `/users/${userId}`, userData);
}
async updateUserTags(userId, tagData) {
return this.makeRequest("PATCH", `/users/${userId}/tags`, tagData);
}
async enrollUserToProduct(enrollmentData) {
return this.makeRequest("POST", "/enrollments", enrollmentData);
}
async unenrollUserFromProduct(unenrollmentData) {
await this.makeRequest("DELETE", "/enrollments", unenrollmentData);
}
async getUser(userId) {
return this.makeRequest("GET", `/users/${userId}`);
}
async getCourse(courseId) {
return this.makeRequest("GET", `/courses/${courseId}`);
}
async getBundle(bundleId) {
return this.makeRequest("GET", `/bundles/${bundleId}`);
}
async getUserEnrollments(userId) {
return this.makeRequest("GET", `/users/${userId}/enrollments`);
}
};
export {
LearnWorldsClient,
OAuth2Client
};