UNPKG

@saumon-brule/ft.js

Version:

Aims to provide a usefull and easy way to use the 42 school's API.

526 lines (504 loc) 15.5 kB
'use strict'; var EventEmitter = require('events'); var http = require('http'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter); // 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__default.default(); } }; var AuthenticatedRequest = class extends http.IncomingMessage { user; }; exports.AuthenticatedRequest = AuthenticatedRequest; exports.FtApp = FtApp; //# sourceMappingURL=index.cjs.map //# sourceMappingURL=index.cjs.map