@saumon-brule/ft.js
Version:
Aims to provide a usefull and easy way to use the 42 school's API.
519 lines (500 loc) • 15.3 kB
JavaScript
import EventEmitter from 'events';
import { IncomingMessage } from 'http';
// src/constants/FtApiBase.ts
var API_BASE = "https://api.intra.42.fr";
// src/constants/FtApiErrors.ts
var FtApiErrorMessages = {
400: "Bad request",
401: "Unauthorized",
403: "Forbidden",
404: "Not found",
422: "Unprocessable entity",
500: "Server issue"
};
// src/generic/request/FtApiFetchError.ts
var FtApiFetchError = class extends Error {
status;
constructor(status) {
super(FtApiErrorMessages[status]);
this.status = status;
}
};
// src/typeguards/checkStatus.ts
function checkStatus(status) {
return status === 400 || status === 401 || status === 403 || status === 404 || status === 422 || status === 500;
}
// src/api/oauth/token.ts
var ROUTE = "/oauth/token";
function appTokenResponseGuard(data) {
return data !== null && typeof data === "object" && Object.keys(data).length === 6 && "access_token" in data && typeof data.access_token === "string" && "token_type" in data && typeof data.token_type === "string" && "expires_in" in data && typeof data.expires_in === "number" && "scope" in data && typeof data.scope === "string" && "created_at" in data && typeof data.created_at === "number" && "secret_valid_until" in data && typeof data.secret_valid_until === "number";
}
async function fetchAppToken(uid, secret) {
const body = new URLSearchParams();
body.append("grant_type", "client_credentials");
body.append("client_id", uid);
body.append("client_secret", secret);
const options = {
method: "POST",
body
};
return fetch(`${API_BASE}${ROUTE}`, options).then(async (response) => {
if (response.ok) {
const data = await response.json();
if (appTokenResponseGuard(data)) {
return data;
}
throw new Error(ROUTE);
}
if (checkStatus(response.status)) {
throw new FtApiFetchError(response.status);
}
throw new Error(`Unexpected status: ${response.status}`);
});
}
function userTokenResponseGuard(data) {
return data !== null && typeof data === "object" && Object.keys(data).length === 7 && "access_token" in data && typeof data.access_token === "string" && "token_type" in data && typeof data.token_type === "string" && "expires_in" in data && typeof data.expires_in === "number" && "refresh_token" in data && typeof data.refresh_token === "string" && "scope" in data && typeof data.scope === "string" && "created_at" in data && typeof data.created_at === "number" && "secret_valid_until" in data && typeof data.secret_valid_until === "number";
}
async function fetchUserToken(code, config) {
const body = new URLSearchParams();
body.append("grant_type", "authorization_code");
body.append("code", code);
body.append("client_id", config.uid);
body.append("client_secret", config.secret);
body.append("redirect_uri", config.redirectURI);
body.append("scope", "identify");
const options = {
method: "POST",
body
};
return fetch(`${API_BASE}${ROUTE}`, options).then(async (response) => {
if (response.ok) {
const data = await response.json();
if (userTokenResponseGuard(data)) {
return data;
}
throw new Error(ROUTE);
}
if (checkStatus(response.status)) {
throw new FtApiFetchError(response.status);
}
throw new Error(`Unexpected status: ${response.status}`);
});
}
async function fetchRefreshUserToken(refreshToken, config) {
const body = new URLSearchParams();
body.append("grant_type", "refresh_token");
body.append("refresh_token", refreshToken);
body.append("client_id", config.uid);
body.append("client_secret", config.secret);
const options = {
method: "POST",
body
};
return fetch(`${API_BASE}${ROUTE}`, options).then(async (response) => {
if (response.ok) {
const data = await response.json();
if (userTokenResponseGuard(data)) {
return data;
}
throw new Error(ROUTE);
}
if (checkStatus(response.status)) {
throw new FtApiFetchError(response.status);
}
throw new Error(`Unexpected status: ${response.status}`);
});
}
// src/app/TokenManager/AppCredentials.ts
var AppCredentials = class {
_tokenData = void 0;
oauthConfig;
_refreshPromise = null;
constructor(oauthConfig) {
this.oauthConfig = oauthConfig;
this.requestNewToken();
}
get _data() {
if (this._tokenData === void 0) throw new Error("Uninitalized token: Cannot access data of uninitialized token");
if (this._tokenData === null) throw new Error("Invalid token: Token is invalid, try to get a new one");
return this._tokenData;
}
get token() {
return this._data.access_token;
}
get type() {
return this._data.token_type;
}
get expiresIn() {
return this._data.expires_in;
}
get createdAt() {
return this._data.created_at;
}
get scope() {
return this._data.scope;
}
get secretValidUntil() {
return this._data.created_at;
}
get expiresAt() {
const data = this._data;
return data.created_at + data.expires_in;
}
async requestNewToken() {
if (this._refreshPromise) return this._refreshPromise;
this._refreshPromise = new Promise(async (resolve, reject) => {
try {
const tokenData = await fetchAppToken(this.oauthConfig.uid, this.oauthConfig.secret);
this._tokenData = tokenData;
resolve();
} catch (error) {
this._tokenData = null;
reject(error);
} finally {
this._refreshPromise = null;
}
});
return this._refreshPromise;
}
get isValid() {
return Date.now() < this.expiresAt;
}
async ensureTokenValidity() {
if (!this._tokenData || !this.isValid)
return this.requestNewToken();
}
async getAccessToken() {
await this.ensureTokenValidity();
return this.token;
}
};
// src/app/TokenManager/AppCredentialsManager.ts
var AppCredentialsManager = class {
_appCredentialsList;
_activeAppTokenIndex = 0;
constructor(configs) {
this._appCredentialsList = configs.map((config) => {
return new AppCredentials(config);
});
}
get _current() {
return this._appCredentialsList[this._activeAppTokenIndex];
}
_shift(offset = 1) {
this._activeAppTokenIndex = (this._activeAppTokenIndex + offset) % this._appCredentialsList.length;
}
get credentials() {
const credentials = this._current;
this._shift();
return credentials;
}
get oauthConfig() {
const oauthConfig = this._current.oauthConfig;
this._shift();
return oauthConfig;
}
get token() {
const token = this._current.token;
this._shift();
return token;
}
get isValid() {
return this._current.isValid;
}
async ensureTokenValidity() {
return this._current.ensureTokenValidity();
}
async getAccessToken() {
const currentCredentials = this._current;
this._shift();
return currentCredentials.getAccessToken();
}
async requestNewToken() {
this._current.requestNewToken();
}
async requestNewTokens() {
const promiseList = [];
this._appCredentialsList.forEach((appCredentials) => promiseList.push(appCredentials.requestNewToken()));
return Promise.all(promiseList);
}
getCredentialByUid(uid) {
return this._appCredentialsList.find((appCredentials) => appCredentials.oauthConfig.uid === uid);
}
};
// src/user/UserCredential.ts
var UserCredential = class {
_tokenData;
oauthConfig;
_refreshPromise = null;
constructor(tokenDataInit, oauthConfig) {
this._tokenData = tokenDataInit;
this.oauthConfig = oauthConfig;
}
get _data() {
if (!this._tokenData) throw new Error("Invalid token: Token is invalid, try to get a new one");
return this._tokenData;
}
get token() {
return this._data.access_token;
}
get refreshToken() {
return this._data.refresh_token;
}
get type() {
return this._data.token_type;
}
get expiresIn() {
return this._data.expires_in;
}
get createdAt() {
return this._data.created_at;
}
get scope() {
return this._data.scope;
}
get secretValidUntil() {
return this._data.secret_valid_until;
}
get expiresAt() {
const data = this._data;
return data.created_at + data.expires_in;
}
async requestNewToken() {
if (this._refreshPromise) return this._refreshPromise;
this._refreshPromise = new Promise(async (resolve, reject) => {
try {
const tokenData = await fetchRefreshUserToken(this.refreshToken, this.oauthConfig);
this._tokenData = tokenData;
resolve();
} catch (error) {
this._tokenData = null;
reject(error);
} finally {
this._refreshPromise = null;
}
});
return this._refreshPromise;
}
get isValid() {
return Date.now() < this.expiresAt;
}
async ensureTokenValidity() {
if (!this.isValid)
await this.requestNewToken();
}
async getAccessToken() {
await this.ensureTokenValidity();
return this.token;
}
};
// src/generic/class/HttpClient.ts
var FtHttpClient = class {
async _createFetchInit(method, options) {
return {
...options,
method
};
}
async get(route, options = {}) {
return fetch(API_BASE + route, await this._createFetchInit("GET", options));
}
async post(route, options) {
return fetch(API_BASE + route, await this._createFetchInit("POST", options));
}
};
// src/app/client/UserHttpClient.ts
var UserHttpClient = class extends FtHttpClient {
user;
constructor(user) {
super();
this.user = user;
}
async _createFetchInit(method, options) {
const userToken = await this.user.credentials.getAccessToken();
const headers = new Headers();
if (!headers.has("Authorization")) {
headers.append("Authorization", `Bearer ${userToken}`);
}
return {
...options,
method,
headers
};
}
};
// src/generic/request/handleResponse.ts
async function handleFtApiResponse(route, response, mapper) {
if (response.ok) {
const data = await response.json();
if (data) {
return mapper(data);
}
throw new Error(route);
}
if (checkStatus(response.status)) {
throw new FtApiFetchError(response.status);
}
throw new Error(`Unexpected status: ${response.status}`);
}
// src/generic/request/FtHeaders.ts
var FtApiHeaders = class extends Headers {
constructor(ftApiHeadersInit, token) {
if (typeof ftApiHeadersInit === "string") {
super();
this.append("Authorization", `Bearer ${ftApiHeadersInit}`);
} else {
super(ftApiHeadersInit);
if (token && !this.has("Authorization")) {
this.append("Authorization", `Bearer ${token}`);
}
}
}
};
// src/api/me.ts
var ROUTE2 = "/v2/me";
async function fetchMe(token, options) {
const headers = new FtApiHeaders(token);
const usedOptions = {
headers,
...options
};
return fetch(`${API_BASE}${ROUTE2}`, usedOptions).then((response) => handleFtApiResponse(ROUTE2, response, (data) => data));
}
// src/user/User.ts
var User = class _User {
id;
ftApp;
credentials;
httpClient;
constructor(ftApp, credentials, data) {
this.ftApp = ftApp;
this.credentials = credentials;
this.httpClient = new UserHttpClient(this);
this.id = data.id;
}
static async create(ftApp, userTokenData, oauthConfig) {
const credentials = new UserCredential(userTokenData, oauthConfig);
const data = await fetchMe(await credentials.getAccessToken());
if (!data.id) throw new Error("No user id after fetch");
return new _User(ftApp, credentials, data);
}
};
// src/app/server/serverResponsesHandler.ts
function redirectResponse(res, url) {
res.statusCode = 302;
res.setHeader("Location", url);
res.end();
}
function sendRawResponse(res, code, message) {
res.statusCode = code;
res.write(message);
res.end();
}
// src/app/UserManager/UserManager.ts
var INTRA_OAUTH_URL = "https://api.intra.42.fr/oauth/authorize";
var UserManager = class {
users = [];
ftApp;
constructor(ftApp) {
this.ftApp = ftApp;
}
getUserById(id) {
return this.users.find((user) => user.id === id);
}
async registerUser(userTokenData, oauthConfig) {
const newUser = await User.create(this.ftApp, userTokenData, oauthConfig);
this.getUserById(newUser.id);
this.users.push(newUser);
this.ftApp.events.emit("userAdd", newUser);
return newUser;
}
authenticate() {
return (_, res) => {
const userConfig = this.ftApp.credentialsManager.oauthConfig;
const params = new URLSearchParams();
params.append("client_id", userConfig.uid);
params.append("redirect_uri", userConfig.redirectURI);
params.append("response_type", "code");
params.append("state", userConfig.uid);
const url = `${INTRA_OAUTH_URL}?${params.toString()}`;
res.statusCode = 302;
res.setHeader("Location", url);
res.end();
};
}
callback({ successPage, errorPage } = {}) {
return async (req, res, next) => {
const isExpress = typeof next === "function";
const url = new URL(req.url ?? "", "http://localhost:3042");
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
if (!code || !state) {
if (errorPage) {
return redirectResponse(res, errorPage);
}
return sendRawResponse(res, 400, "Authentification Error");
}
const appCredentials = this.ftApp.credentialsManager.getCredentialByUid(state);
if (appCredentials === void 0) {
if (errorPage) {
return redirectResponse(res, errorPage);
}
return sendRawResponse(res, 400, "Authentification Error");
}
const oauthConfig = appCredentials.oauthConfig;
try {
const tokenData = await fetchUserToken(code, oauthConfig);
req.user = await this.registerUser(tokenData, oauthConfig);
if (isExpress) return next();
if (successPage) {
return redirectResponse(res, successPage);
}
return sendRawResponse(res, 200, "Authentification Success");
} catch (error) {
if (errorPage) {
redirectResponse(res, errorPage);
} else {
sendRawResponse(res, 500, "Server Error");
}
throw error;
}
};
}
};
// src/app/client/AppHttpClient.ts
var AppHttpClient = class extends FtHttpClient {
ftApp;
constructor(ftApp) {
super();
this.ftApp = ftApp;
}
async _createFetchInit(method, options) {
const appToken = await this.ftApp.credentialsManager.getAccessToken();
const headers = new Headers();
headers.append("Authorization", `Bearer ${appToken}`);
return {
...options,
method,
headers
};
}
};
// src/app/App.ts
var FtApp = class {
credentialsManager;
userManager;
httpClient;
events;
constructor(configs) {
this.credentialsManager = new AppCredentialsManager(configs);
this.userManager = new UserManager(this);
this.httpClient = new AppHttpClient(this);
this.events = new EventEmitter();
}
};
var AuthenticatedRequest = class extends IncomingMessage {
user;
};
export { AuthenticatedRequest, FtApp };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map