diff --git a/README.md b/README.md index 4f3273c..9cafd4e 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,11 @@ npm install ccash-client-js ```js import { CCashClient } from 'ccash-client-js'; +process.env.CCASH_API_BASE_URL = 'https://your.ccash.api'; + const client = new CCashClient(); -client.balance('twix'); +console.log(await client.balance('blinkblinko')); ``` ## Examples diff --git a/examples/node/.env b/examples/node/.env new file mode 100644 index 0000000..41ab2d0 --- /dev/null +++ b/examples/node/.env @@ -0,0 +1 @@ +CCASH_API_BASE_URL=https://wtfisthis.tech/BankF diff --git a/examples/node/index.js b/examples/node/index.js index bdf46b4..2327dee 100644 --- a/examples/node/index.js +++ b/examples/node/index.js @@ -1,10 +1,20 @@ +require('dotenv').config(); const { CCashClient } = require('ccash-client-js'); -const client = new CCashClient() +const user = 'blinkblinko'; +const pass = 'TestPassword'; + +const client = new CCashClient(); async function main() { - const balance = await client.balance('twix'); - console.log(`Balance: ${balance}`) + const createdUser = await client.addUser(user, pass); + console.log('User created', createdUser); + + const balance = await client.balance(user); + console.log(`Balance: ${balance}`); + + const deletedUser = await client.deleteUser(user, pass); + console.log('User deleated', deletedUser); } -main() +main(); diff --git a/examples/node/package.json b/examples/node/package.json index a3035c1..7b1887d 100644 --- a/examples/node/package.json +++ b/examples/node/package.json @@ -7,6 +7,7 @@ "start": "node index.js" }, "dependencies": { - "ccash-client-js": "file:../.." + "ccash-client-js": "file:../..", + "dotenv": "^10.0.0" } } diff --git a/examples/node/yarn.lock b/examples/node/yarn.lock index 0baf97d..f6266d5 100644 --- a/examples/node/yarn.lock +++ b/examples/node/yarn.lock @@ -14,6 +14,11 @@ axios@^0.21.1: dependencies: axios "^0.21.1" +dotenv@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" + integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== + follow-redirects@^1.10.0: version "1.14.1" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43" diff --git a/examples/web/.env b/examples/web/.env new file mode 100644 index 0000000..dbc613b --- /dev/null +++ b/examples/web/.env @@ -0,0 +1,2 @@ +SKIP_PREFLIGHT_CHECK=true +CCASH_API_BASE_URL=https://wtfisthis.tech/BankF diff --git a/examples/web/src/App.js b/examples/web/src/App.js index 02e9465..020dc07 100644 --- a/examples/web/src/App.js +++ b/examples/web/src/App.js @@ -2,14 +2,16 @@ import React, { useEffect, useState } from 'react'; import { CCashClient } from 'ccash-client-js'; import './App.css'; -const client = new CCashClient(); +const client = new CCashClient( + process.env.CCASH_API_BASE_URL || 'https://wtfisthis.tech/BankF' +); function App() { const [balance, setBalance] = useState(0); useEffect(() => { (async function getBalance() { - setBalance(await client.balance('twix')); + setBalance(await client.balance('blinkblinko')); })(); }, []); diff --git a/examples/web/yarn.lock b/examples/web/yarn.lock index 015b8a1..4cf29a2 100644 --- a/examples/web/yarn.lock +++ b/examples/web/yarn.lock @@ -3163,6 +3163,7 @@ case-sensitive-paths-webpack-plugin@2.3.0: version "0.0.0" dependencies: axios "^0.21.1" + class-transformer "^0.4.0" chalk@2.4.2, chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" @@ -3266,6 +3267,11 @@ cjs-module-lexer@^0.6.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz#4186fcca0eae175970aee870b9fe2d6cf8d5655f" integrity sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw== +class-transformer@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.4.0.tgz#b52144117b423c516afb44cc1c76dbad31c2165b" + integrity sha512-ETWD/H2TbWbKEi7m9N4Km5+cw1hNcqJSxlSYhsLsNjQzWWiZIYA1zafxpK9PwVfaZ6AqR5rrjPVUBGESm5tQUA== + class-utils@^0.3.5: version "0.3.6" resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" diff --git a/package.json b/package.json index 8ee58ad..b21f0a1 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "test": "jest" }, "dependencies": { - "axios": "^0.21.1" + "axios": "^0.21.1", + "class-transformer": "^0.4.0" }, "devDependencies": { "@babel/core": "^7.14.5", diff --git a/src/CCashClient.exceptions.ts b/src/CCashClient.exceptions.ts new file mode 100644 index 0000000..826bcf6 --- /dev/null +++ b/src/CCashClient.exceptions.ts @@ -0,0 +1,69 @@ +import { Exception } from './Exception'; + +export class BaseUrlMissingException extends Exception { + constructor() { + super('base url missing'); + } +} + +export class UserNotFoundException extends Exception { + constructor() { + super('user not found'); + } +} + +export class WrongPasswordException extends Exception { + constructor() { + super('wrong password'); + } +} + +export class InvalidRequestException extends Exception { + constructor() { + super('invalid request'); + } +} + +export class WrongAdminPasswordException extends Exception { + constructor() { + super('wrong admin password'); + } +} + +export class NameTooLongException extends Exception { + constructor() { + super('name too long'); + } +} + +export class UserAlreadyExistsException extends Exception { + constructor() { + super('user already exists'); + } +} + +export class InsufficientFundsException extends Exception { + constructor() { + super('insufficient funds'); + } +} + +export enum ErrorCodes { + UserNotFound = -1, + WrongPassword = -2, + InvalidRequest = -3, + WrongAdminPassword = -4, + NameTooLong = -5, + UserAlreadyExists = -6, + InsufficientFunds = -7, +} + +export const ExceptionMap = { + [ErrorCodes.UserNotFound]: UserNotFoundException, + [ErrorCodes.WrongPassword]: WrongPasswordException, + [ErrorCodes.InvalidRequest]: InvalidRequestException, + [ErrorCodes.WrongAdminPassword]: WrongAdminPasswordException, + [ErrorCodes.NameTooLong]: NameTooLongException, + [ErrorCodes.UserAlreadyExists]: UserAlreadyExistsException, + [ErrorCodes.InsufficientFunds]: InsufficientFundsException, +}; diff --git a/src/CCashClient.ts b/src/CCashClient.ts index a8cc120..425f280 100644 --- a/src/CCashClient.ts +++ b/src/CCashClient.ts @@ -1,5 +1,166 @@ -export class CCashClient { - balance(user: string): Promise { - return Promise.resolve(10); +import axios, { AxiosInstance, AxiosResponse } from 'axios'; +import { plainToClass } from 'class-transformer'; +import { ICCashClient, User } from './CCashClient.types'; +import { + BaseUrlMissingException, + ExceptionMap, + ErrorCodes, +} from './CCashClient.exceptions'; + +export class CCashClient implements Partial { + /** TODO: not partial **/ + http: AxiosInstance; + + constructor(baseURL: string | undefined = process.env.CCASH_API_BASE_URL) { + if (!baseURL) { + throw new BaseUrlMissingException(); + } + + this.http = axios.create({ + baseURL, + }); } + + balance(user: string): Promise { + return this.http + .get(`/${user}/bal`) + .then((response) => this.handleError(response) || response.data.value); + } + + log( + user: string, + pass: string, + transactionCount: number = 10 + ): Promise { + return this.http + .get(`/${user}/bal`, { + headers: { Password: pass }, + params: { n: transactionCount }, + }) + .then((response) => this.handleError(response) || response.data.value); + } + + sendFunds( + user: string, + pass: string, + to: string, + amount: number + ): Promise { + return this.http + .post(`/${user}/send/${to}`, { + headers: { Password: pass }, + params: { amount }, + }) + .then((response) => this.handleError(response) || amount); + } + + verifyPassword(user: string, pass: string): Promise { + return this.http + .get(`/${user}/pass/verify`, { headers: { Password: pass } }) + .then((response) => this.handleError(response) || response.data.value); + } + + changePassword(user: string, pass: string, newPass: string): Promise { + return this.http + .patch( + `/${user}/pass/change`, + { password: newPass }, + { headers: { Password: pass } } + ) + .then( + (response) => + this.handleError(response) || this.serialize(User, { user }) + ); + } + + setBal(user: string, pass: string, amount: number): Promise { + return this.http + .patch(`/admin/${user}/bal`, undefined, { + headers: { Password: pass }, + params: { amount }, + }) + .then((response) => this.handleError(response) || amount); + } + + help(): Promise { + return this.http.get('/help').then((response) => response.data); + } + + close(pass: string): Promise { + return this.http + .post('/close', undefined, { headers: { Password: pass } }) + .then((response) => this.handleError(response) || true); + } + + contains(user: string): Promise { + return this.http + .get(`/contains/${user}`) + .then( + (response) => this.handleError(response) || response.data.value || false + ); + } + + adminVerifyPass(pass: string): Promise { + return this.http + .get('/admin/verify') + .then( + (response) => this.handleError(response) || response.data.value || false + ); + } + + addUser(user: string, pass: string): Promise { + return this.http + .post(`/user/${user}`, undefined, { headers: { Password: pass } }) + .then( + (response) => + this.handleError(response) || this.serialize(User, { user }) + ); + } + + adminAddUser( + user: string, + pass: string, + initialBalance: number + ): Promise { + return this.http + .post(`/user/${user}`, undefined, { + headers: { Password: pass }, + params: { init_bal: initialBalance }, + }) + .then( + (response) => + this.handleError(response) || this.serialize(User, { user }) + ); + } + + deleteUser(user: string, pass: string): Promise { + return this.http + .delete(`/user/${user}`, { headers: { Password: pass } }) + .then( + (response) => + this.handleError(response) || this.serialize(User, { user }) + ); + } + + adminDeleteUser(user: string, pass: string): Promise { + return this.http + .delete(`/user/${user}`, { headers: { Password: pass } }) + .then( + (response) => + this.handleError(response) || this.serialize(User, { user }) + ); + } + + private handleError(response: AxiosResponse): false { + if ( + response.data.value && + Object.values(ErrorCodes).includes(response.data.value) + ) { + throw new ExceptionMap[response.data.value as ErrorCodes](); + } + + return false; + } + + private serialize = plainToClass; } diff --git a/src/CCashClient.types.ts b/src/CCashClient.types.ts new file mode 100644 index 0000000..d3b37d7 --- /dev/null +++ b/src/CCashClient.types.ts @@ -0,0 +1,36 @@ +import { User } from './User'; + +export { User }; + +export interface ICCashClient { + // Usage + balance(user: string): Promise; + log(user: string, pass: string, transactionCount?: number): Promise; + sendFunds( + user: string, + pass: string, + to: string, + amount: number + ): Promise; + verifyPassword(user: string, pass: string): Promise; + + // Meta usage + changePassword(user: string, pass: string, newPass: string): Promise; + setBal(user: string, pass: string, amount: number): Promise; + + // System usage + help(): Promise; + close(pass: string): Promise; + contains(user: string): Promise; + adminVerifyPass(pass: string): Promise; + + // User management + addUser(user: string, pass: string): Promise; + adminAddUser( + user: string, + pass: string, + initialBalance: number + ): Promise; + deleteUser(user: string, pass: string): Promise; + adminDeleteUser(user: string, pass: string): Promise; +} diff --git a/src/Exception.ts b/src/Exception.ts new file mode 100644 index 0000000..ad7b0b6 --- /dev/null +++ b/src/Exception.ts @@ -0,0 +1 @@ +export class Exception extends Error {} diff --git a/src/User.ts b/src/User.ts new file mode 100644 index 0000000..56063a0 --- /dev/null +++ b/src/User.ts @@ -0,0 +1,7 @@ +import { Exclude, Expose } from 'class-transformer'; + +@Exclude() +export class User { + @Expose() + user!: string; +} diff --git a/src/index.ts b/src/index.ts index 2dec000..1bb5214 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,3 @@ -export { CCashClient } from './CCashClient'; +export * from './CCashClient'; +export * from './CCashClient.types'; +export * from './CCashClient.exceptions'; diff --git a/test/CCashClient.spec.ts b/test/CCashClient.spec.ts index 2d804de..5dafa2c 100644 --- a/test/CCashClient.spec.ts +++ b/test/CCashClient.spec.ts @@ -12,7 +12,7 @@ describe('CCashClient', () => { describe('balance', () => { it('returns an integer', async () => { - expect(await client.balance('twix')).toEqual(10); + expect(await client.balance('blinkblinko')).toEqual(10); }); }); }); diff --git a/tsconfig.json b/tsconfig.json index e622513..3395b32 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,11 +3,13 @@ "outDir": "./dist/cjs", "target": "es2015", "module": "commonjs", + "declaration": true, "strict": true, "noImplicitAny": true, "moduleResolution": "node", "allowSyntheticDefaultImports": true, - "forceConsistentCasingInFileNames": true + "forceConsistentCasingInFileNames": true, + "experimentalDecorators": true }, "include": ["src/**/*"], "paths": { diff --git a/yarn.lock b/yarn.lock index 0a51e49..a0a50cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1546,6 +1546,11 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.1.tgz#2fd46d9906a126965aa541345c499aaa18e8cd73" integrity sha512-jVamGdJPDeuQilKhvVn1h3knuMOZzr8QDnpk+M9aMlCaMkTDd6fBWPhiDqFvFZ07pL0liqabAiuy8SY4jGHeaw== +class-transformer@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.4.0.tgz#b52144117b423c516afb44cc1c76dbad31c2165b" + integrity sha512-ETWD/H2TbWbKEi7m9N4Km5+cw1hNcqJSxlSYhsLsNjQzWWiZIYA1zafxpK9PwVfaZ6AqR5rrjPVUBGESm5tQUA== + cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"