Prototype API (#2)

* Add oauth2 client

* Update LICENSE.txt

* Add oauth2 types

* Prepare for publish
This commit is contained in:
Guilherme Werner
2023-12-31 15:15:16 -03:00
committed by GitHub
parent d3ab72a0e3
commit ebd04dbc78
10 changed files with 1382 additions and 1473 deletions

1
.gitattributes vendored
View File

@ -1,2 +1 @@
# Auto detect text files and perform LF normalization
* text=auto eol=lf * text=auto eol=lf

37
.gitignore vendored
View File

@ -1,11 +1,40 @@
__pycache__/
.gradle/
.idea/
.metals/
.next/ .next/
.parcel-cache/ .parcel-cache/
[Bb]uild/ .vs/
[Ss]aved/ .vscode/
bin/
binaries/
build/
node_modules/ node_modules/
obj/
saved/
target/
*.wasm
.DS_Store .DS_Store
.env .env
*.crt
*.csproj
*.filters
*.fsproj
*.key
*.log
*.make
*.mwb.bak
*.pem
*.sln
*.user
*.vcxproj
*.vpp.*
*.wasm
*.xcodeproj
*.xcworkspace
Cargo.lock
desktop.ini desktop.ini
yarn-error.log keystore.jks
local.properties
Makefile
next-env.d.ts

View File

@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright (c) TribuFu. All Rights Reserved. Copyright (c) Tribufu. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@ -1,3 +0,0 @@
// Copyright (c) TribuFu. All Rights Reserved.
export const VERSION = "0.0.0";

View File

@ -1,23 +1,35 @@
{ {
"name": "tribufu", "name": "tribufu",
"version": "0.0.2", "version": "0.1.0",
"description": "TribuFu JS SDK", "description": "Tribufu JS SDK",
"repository": "https://github.com/TribuFu/SDK-JS", "repository": "https://github.com/Tribufu/TribufuJs",
"author": "TribuFu <contact@tribufu.com>", "author": "Tribufu <contact@Tribufu.com>",
"license": "Apache-2.0", "license": "Apache-2.0",
"source": "Source/index.ts", "type": "module",
"main": "Build/main.js", "exports": {
"module": "Build/module.js", "import": "./build/index.mjs",
"types": "Build/index.d.ts", "require": "./build//index.cjs"
},
"scripts": { "scripts": {
"watch": "parcel watch --cache-dir Saved/Cache", "clean": "rimraf build",
"build": "parcel build --cache-dir Saved/Cache", "build": "yarn clean && tsc && node scripts/esbuild.js"
"prepare": "npm run build" },
"dependencies": {
"@types/express": "^4.17.21",
"axios": "^1.6.3",
"camelcase-keys": "^9.1.2",
"fp-ts": "^2.16.1",
"json-bigint": "^1.0.0",
"snakecase-keys": "^5.5.0",
"uuid": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
"@parcel/packager-ts": "2.8.3", "@types/json-bigint": "^1.0.4",
"@parcel/transformer-typescript-types": "2.8.3", "@types/uuid": "^9.0.7",
"parcel": "^2.8.3", "cross-env": "^7.0.3",
"typescript": ">=3.0.0" "esbuild": "^0.19.10",
"esbuild-node-externals": "^1.12.0",
"rimraf": "^5.0.5",
"typescript": "^5.3.3"
} }
} }

30
scripts/esbuild.js Normal file
View File

@ -0,0 +1,30 @@
// Copyright (c) Tribufu. All Rights Reserved.
import { build } from "esbuild";
import { nodeExternalsPlugin } from "esbuild-node-externals";
const baseConfig = {
entryPoints: ["src/index.ts"],
platform: "neutral",
target: "node18",
bundle: true,
minify: true,
sourcemap: false,
legalComments: "linked",
plugins: [nodeExternalsPlugin()],
};
const moduleConfig = {
...baseConfig,
outfile: "build/index.mjs",
format: "esm",
};
const legacyConfig = {
...baseConfig,
outfile: "build/index.cjs",
format: "cjs",
};
build(moduleConfig).catch(() => process.exit(1));
build(legacyConfig).catch(() => process.exit(1));

536
src/index.ts Normal file
View File

@ -0,0 +1,536 @@
// Copyright (c) Tribufu. All Rights Reserved.
import axios, { AxiosInstance } from "axios";
import { OAuth2TokenRequest, OAuth2TokenResponse, OAuth2GrantType } from "./oauth2.ts";
export const TRIBUFU_VERSION = "0.0.0";
export interface User {
id: string;
uuid: string;
name: string;
displayName: string;
email?: string;
emailConfirmed: boolean;
type: string;
organizationId?: string;
privateFlags: string;
publicFlags: string;
official: boolean;
level: number;
experience: number;
publicBirthday: boolean;
birthday?: Date;
points: number;
parentalControl: boolean;
firstName?: string;
lastName?: string;
phone?: string;
phoneConfirmed: boolean;
location?: string;
timezone?: string;
language?: string;
currency?: string;
deleted: boolean;
lastIpAddress?: string;
registrationIpAddress?: string;
failedLogins: number;
twoFactorEnabled: boolean;
twoFactorKey?: string;
photoUrl: string;
bannerUrl: string;
status: string;
lastOnline?: Date;
biography?: string;
views: number;
created: Date;
updated?: Date;
}
export interface SessionInfo {
user: User;
token: OAuth2TokenResponse;
};
export interface LoginRequest {
login: string;
password: string;
persistent: boolean | undefined;
state: string | null | undefined;
};
export type HeaderMap = {
[key: string]: string
};
export type CookieMap = {
[key: string]: string;
};
export enum RuntimeEnvironment {
Browser,
Node,
Other,
}
/*
export type NextApiCookieContext = { req: NextApiRequest };
export type NextPageCookieContext = Pick<NextPageContext, "req">;
export type ExpressCookieContext = { req: Request; };
export type CookieContext = NextApiCookieContext | NextPageCookieContext | ExpressCookieContext | null | undefined;
*/
export type ServerContext = any | null | undefined;
export const TRIBUFU_API_URL = "https://api.tribufu.com";
export enum CredentialsType {
Anonymous,
ApiKey,
Client,
}
export enum TokenType {
ApiKey,
Basic,
Bearer,
}
export interface ApiKey {
kind: CredentialsType.ApiKey;
apiKey: string;
}
export interface ClientCredentials {
kind: CredentialsType.Client;
clientId: string;
clientSecret: string;
}
export type Credentials = ApiKey | ClientCredentials;
export class TribufuApi {
private credentials: Credentials | null = null;
protected accessToken: string | null = null;
protected refreshToken: string | null = null;
private tokenType: TokenType | null = null;
protected readonly http: AxiosInstance;
constructor(credentials: Credentials | null) {
if (credentials) {
switch (credentials.kind) {
case CredentialsType.ApiKey:
this.setApiKey(credentials.apiKey);
break;
case CredentialsType.Client:
this.setClient(credentials.clientId, credentials.clientSecret);
break;
}
}
let http = axios.create({
baseURL: TribufuApi.getBaseUrl(),
headers: TribufuApi.defaultHeaders(),
});
if (TribufuApi.debugEnabled()) {
http.interceptors.request.use((config) => {
console.log(`(TribufuApi) ${config.method?.toUpperCase()} ${config.baseURL}${config.url}`);
return config;
});
}
this.http = http;
}
public static default(): TribufuApi {
return new TribufuApi(null);
}
private static debugEnabled(): boolean {
return process.env.NODE_ENV ? process.env.NODE_ENV === "development" : false;
}
private static getBaseUrl(): string {
const baseUrl = process.env.TRIBUFU_API_URL || null;
return TribufuApi.debugEnabled() && baseUrl ? baseUrl : TRIBUFU_API_URL;
}
private static userAgent(): string {
const targetTriple = "JavaScript";
const userAgent = `Tribufu/${TRIBUFU_VERSION} (+https://api.tribufu.com; ${targetTriple}`;
return userAgent;
}
private static defaultHeaders(): HeaderMap {
const headers = {
"User-Agent": TribufuApi.userAgent(),
"X-Tribufu-Language": "javascript",
"X-Tribufu-Version": TRIBUFU_VERSION,
};
return headers;
}
private static detectRuntime(): RuntimeEnvironment {
if (typeof window !== "undefined") {
return RuntimeEnvironment.Browser;
}
if (typeof process !== "undefined" && process?.versions?.node) {
return RuntimeEnvironment.Node;
}
return RuntimeEnvironment.Other;
}
public static isBrowser(): boolean {
return TribufuApi.detectRuntime() === RuntimeEnvironment.Browser;
}
public static isNode(): boolean {
return TribufuApi.detectRuntime() === RuntimeEnvironment.Node;
}
public static withApiKey(apiKey: string): TribufuApi {
return new TribufuApi({
kind: CredentialsType.ApiKey,
apiKey,
});
}
public static withClient(clientId: string, clientSecret: string): TribufuApi {
return new TribufuApi({
kind: CredentialsType.Client,
clientId,
clientSecret,
});
}
public static withApiKeyFromEnv(): TribufuApi {
const apiKey = process.env.TRIBUFU_API_URL_KEY;
if (apiKey) {
return TribufuApi.withApiKey(apiKey);
}
return TribufuApi.default();
}
public static withClientFromEnv(): TribufuApi {
const clientId = process.env.TRIBUFU_CLIENT_ID;
const clientSecret = process.env.TRIBUFU_CLIENT_SECRET;
if (clientId && clientSecret) {
return TribufuApi.withClient(clientId, clientSecret);
}
return TribufuApi.default();
}
/*
public static fromContext(context: ServerContext): TribufuApi {
return TribufuApi.fromCookies(nookies.get(context));
}
*/
public static fromCookies(cookies: CookieMap): TribufuApi {
let api = TribufuApi.withClientFromEnv();
const accessToken = cookies["access_token"];
if (accessToken) {
api?.setBearerToken(accessToken);
}
return api;
}
public setAnonymous() {
this.credentials = null;
}
public setApiKey(apiKey: string) {
this.credentials = { kind: CredentialsType.ApiKey, apiKey };
}
public setClient(clientId: string, clientSecret: string) {
this.credentials = { kind: CredentialsType.Client, clientId, clientSecret };
this.setBasicToken(Buffer.from(`${this.credentials.clientId}:${this.credentials.clientSecret}`, "binary").toString("base64"));
}
public setBasicToken(basicToken: string) {
this.tokenType = TokenType.Basic;
this.accessToken = basicToken;
}
public setBearerToken(accessToken: string, refreshToken?: string | null) {
this.tokenType = TokenType.Bearer;
this.accessToken = accessToken;
this.refreshToken = refreshToken || null;
}
private getHeaders(): HeaderMap {
let headers: HeaderMap = {};
if (this.credentials?.kind === CredentialsType.ApiKey) {
headers["Authorization"] = `ApiKey ${this.credentials.apiKey}`;
return headers;
}
switch (this.tokenType) {
case TokenType.Basic:
headers["Authorization"] = `Basic ${this.accessToken}`;
break;
case TokenType.Bearer:
headers["Authorization"] = `Bearer ${this.accessToken}`;
break;
}
return headers;
}
public async getTokenUsingAuthorizationCode(authorizationCode: string): Promise<OAuth2TokenResponse | null> {
return await this.getToken("authorization_code", authorizationCode, null, null);
}
public async getTokenUsingDeviceCode(deviceCode: string): Promise<OAuth2TokenResponse | null> {
return await this.getToken("device_code", deviceCode, null, null);
}
public async getTokenUsingRefreshToken(refreshToken: string): Promise<OAuth2TokenResponse | null> {
return await this.getToken("refresh_token", refreshToken, null, null);
}
public async getTokenUsingPassword(username: string, password: string): Promise<OAuth2TokenResponse | null> {
return await this.getToken("password", password, null, username);
}
public async getTokenUsingPasskey(username: string, passkey: string): Promise<OAuth2TokenResponse | null> {
return await this.getToken("passkey", passkey, null, username);
}
public async getTokenUsingClientCredentials(serverId?: string | null): Promise<OAuth2TokenResponse | null> {
return await this.getToken("client_credentials", null, serverId ? "server_id" : null, serverId || null);
}
private async getToken(grantType: OAuth2GrantType, grantValue: string | null, subjectKey: string | null, subjectValue: string | null): Promise<OAuth2TokenResponse | null> {
try {
if (this.credentials?.kind !== CredentialsType.Client) {
return null;
}
const credentials = this.credentials as ClientCredentials;
const requestBody: OAuth2TokenRequest = {
grant_type: grantType,
code: grantType === "authorization_code" ? grantValue : null,
refresh_token: grantType === "refresh_token" ? grantValue : null,
username: grantType === "password" || grantType === "passkey" ? subjectValue : null,
password: grantType === "password" ? grantValue : null,
passkey: grantType === "passkey" ? grantValue : null,
client_id: credentials.clientId,
client_secret: credentials.clientSecret,
}
const params = subjectKey && subjectValue ? `?${subjectKey}=${subjectValue}` : "";
const url = `/v1/oauth2/token${params}`;
const headers = this.getHeaders();
headers["Content-Type"] = "application/x-www-form-urlencoded";
const response = await this.http.post(url, requestBody, { headers });
if (response.status !== 200) {
return null;
}
const responseBody = response.data as OAuth2TokenResponse;
return responseBody;
} catch (error) {
console.error(error)
return null;
}
}
public async getUserInfo(): Promise<User | null> {
try {
const headers = this.getHeaders();
const response = await this.http.get(`/v1/oauth2/userinfo`, { headers });
if (response.status !== 200) {
return null;
}
const user = response.data as User;
return user;
} catch (error) {
console.error(error)
return null;
}
}
public async getGames(page: number = 1): Promise<any[]> {
try {
const headers = this.getHeaders();
const response = await this.http.get(`/v1/packages?page=${page}`, { headers });
if (response.status !== 200) {
return [];
}
const servers = response.data as any[];
return servers;
} catch (error) {
console.error(error)
return [];
}
}
public async getGameyId(id: string): Promise<any> {
try {
const headers = this.getHeaders();
const response = await this.http.get(`/v1/packages/${id}`, { headers });
if (response.status !== 200) {
return [];
}
const server = response.data as any;
return server;
} catch (error) {
console.error(error)
return null;
}
}
public async getServers(page: number = 1): Promise<any[]> {
try {
const headers = this.getHeaders();
const response = await this.http.get(`/v1/servers?page=${page}`, { headers });
if (response.status !== 200) {
return [];
}
const servers = response.data as any[];
return servers;
} catch (error) {
console.error(error)
return [];
}
}
public async getServer(idOrAddress: string): Promise<any> {
if (isNaN(idOrAddress as any)) {
return await this.getServerByAddress(idOrAddress);
}
return await this.getServerById(idOrAddress);
}
public async getServerById(id: string): Promise<any> {
try {
const headers = this.getHeaders();
const response = await this.http.get(`/v1/servers/${id}`, { headers });
if (response.status !== 200) {
return [];
}
const server = response.data as any;
return server;
} catch (error) {
console.error(error)
return null;
}
}
public async getServerByAddress(address: string): Promise<any> {
try {
const headers = this.getHeaders();
const response = await this.http.get(`/v1/servers/address/${address}`, { headers });
if (response.status !== 200) {
return [];
}
const server = response.data as any;
return server;
} catch (error) {
console.error(error)
return null;
}
}
public async getUserById(id: string): Promise<any> {
try {
const headers = this.getHeaders();
const response = await this.http.get(`/v1/users/${id}`, { headers });
if (response.status !== 200) {
return null;
}
const user = response.data as any;
return user;
} catch (error) {
console.error(error)
return null;
}
}
public async getUserByUuid(uuid: string): Promise<any[]> {
return await this.getUserByKey("uuid", uuid);
}
public async getUserByName(name: string): Promise<any[]> {
return await this.getUserByKey("name", name);
}
public async getUserByEmail(email: string): Promise<any[]> {
return await this.getUserByKey("email", email);
}
private async getUserByKey(key: string, value: string): Promise<any[]> {
try {
const headers = this.getHeaders();
const response = await this.http.get(`/v1/users/?${key}=${value}`, { headers });
if (response.status !== 200) {
return [];
}
const users = response.data as any[];
return users;
} catch (error) {
console.error(error)
return [];
}
}
public async getUserServers(userId: string): Promise<any[]> {
try {
const headers = this.getHeaders();
const response = await this.http.get(`/v1/users/${userId}/servers`, { headers });
if (response.status !== 200) {
return [];
}
const servers = response.data as any[];
return servers;
} catch (error) {
console.error(error)
return [];
}
}
}

55
src/oauth2.ts Normal file
View File

@ -0,0 +1,55 @@
// Copyright (c) Tribufu. All Rights Reserved.
export type OAuth2ClientType = "web" | "native";
export type OAuth2GrantType = "authorization_code" | "client_credentials" | "device_code" | "password" | "passkey" | "refresh_token";
export type OAuth2ResponseType = "code" | "token";
export type OAuth2TokenHintType = "refresh_token" | "access_token";
export type OAuth2TokenType = "bearer";
export interface OAuth2AuthorizeRequest {
response_type: OAuth2ResponseType;
client_id: string;
client_secret: string;
scope?: string | null;
redirect_uri: string;
state?: string | null;
};
export interface OAuth2CodeResponse {
code: string;
state?: string | null;
};
export interface OAuth2TokenRequest {
grant_type: OAuth2GrantType;
code?: string | null;
refresh_token?: string | null;
username?: string | null;
password?: string | null;
passkey?: string | null;
client_id: string;
client_secret: string;
redirect_uri?: string | null;
};
export interface OAuth2TokenResponse {
token_type: OAuth2TokenType;
access_token: string;
refresh_token?: string | null;
scope?: string | null;
state?: string | null;
expires_in: number;
};
export interface OAuth2RevokeRequest {
token: string;
token_type_hint: OAuth2TokenHintType;
};
export interface OAuth2IntrospectionResponse {
active: boolean;
client_id?: string | null;
username?: string | null;
scope?: string | null;
exp?: number | null;
};

29
tsconfig.json Normal file
View File

@ -0,0 +1,29 @@
{
"compilerOptions": {
"strict": true,
"target": "ESNext",
"declaration": true,
"emitDeclarationOnly": true,
"noImplicitAny": false,
"moduleResolution": "Node",
"esModuleInterop": true,
"allowImportingTsExtensions": true,
"outDir": "build",
"lib": [
"ESNext",
"DOM"
],
"paths": {
"@/*": [
"./*"
]
}
},
"include": [
"**/*.ts",
],
"exclude": [
"build",
"node_modules",
],
}

2120
yarn.lock

File diff suppressed because it is too large Load Diff