feat: server authentication (#38)

* feat:  server sso auth

* chore: 🔧 continue sso integration

* fix: 🐛 correct filter

* chore: 🔧 add resolver types

* chore: 🔧 linter sort imports

* fix: 🐛 duplicate imports

* chore: 🔧 change auth context

* feat:  add accounts read and query

* fix: 🐛 small changes

* chore: 🔧 continue integration

* chore: 🔧 add auth

* chore: 🔧 switch to new context

* fix: 🐛 imports and account context

* chore: 🔧 add session token query

* chore: 🔧 change server auth

* chore: 🔧 add sso to server accounts query

* chore: 🔧 cleanup and small fixes

* chore: 🔧 separate generated files

* refactor: ♻️ change graphql imports

* fix: 🐛 add id to params

* fix: 🐛 auth changes

* chore: 🔧 improve logging and account fixes

* chore: 🔧 change footer

* chore: 🔧 remove console logs

* chore: 🔧 add spacing

* fix: 🐛 final changes
This commit is contained in:
Anthony Potdevin 2020-05-19 07:50:16 +02:00 committed by GitHub
parent 0394209b80
commit 9d73c30fb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
240 changed files with 8231 additions and 6293 deletions

View File

@ -113,12 +113,57 @@ npm run dev -> npm run dev:compatible
You can define some environment variables that ThunderHub can start with. To do this create a `.env` file in the root directory with the following parameters:
```js
LOG_LEVEL = 'error' | 'warn' | 'info' | 'http' | 'verbose' | 'debug' | 'silly' //Default: 'info'
THEME = 'dark' | 'light'; // Default: 'dark'
CURRENCY = 'sat' | 'btc' | 'eur' | 'usd'; // Default: 'sat'
FETCH_PRICES = true | false // Default: true
FETCH_FEES = true | false // Default: true
HODL_KEY='[Key provided by HodlHodl]' //Default: ''
BASE_PATH='[Base path where you want to have thunderhub running i.e. '/btcpay']' //Default: '/'
HODL_KEY = '[Key provided by HodlHodl]' //Default: ''
BASE_PATH = '[Base path where you want to have thunderhub running i.e. '/btcpay']' //Default: '/'
```
### SSO Account
You can define an account to work with SSO cookie authentication by adding the following parameters in the `.env` file:
```js
COOKIE_PATH = '/path/to/cookie/file/.cookie'; // i.e. '/data/.cookie'
SSO_SERVER_URL = 'url and port to node'; // i.e. '127.0.0.1:10009'
SSO_CERT_PATH = '/path/to/tls/certificate'; // i.e. '\lnd\alice\tls.cert'
SSO_MACAROON_PATH = '/path/to/macaroon/folder'; //i.e. '\lnd\alice\data\chain\bitcoin\regtest\'
```
To login to this account you must add the cookie file content to the end of your ThunderHub url. For example:
```
http://localhost:3000?token=[COOKIE]
```
Replace `[COOKIE]` with the contents of the `.cookie` file.
### Server Accounts
You can add accounts on the server by adding this parameter to the `.env` file:
```js
ACCOUNT_CONFIG_PATH = '/path/to/config/file.yaml'; // i.e. '/data/thubConfig.yaml'
```
You must also add a YAML file at that location with the following format:
```yaml
masterPassword: 'password' # Default password unless defined in account
accounts:
- name: 'Account 1'
serverUrl: 'url:port'
macaroonPath: '/path/to/admin.macaroon'
certificatePath: '/path/to/tls.cert'
password: 'password for account 1'
- name: 'Account 2'
serverUrl: 'url:port'
macaroonPath: '/path/to/admin.macaroon'
certificatePath: '/path/to/tls.cert'
# password: Leave without password and it will use the master password
```
### Fetching prices and fees

252
api/helpers/fileHelpers.ts Normal file
View File

@ -0,0 +1,252 @@
import fs from 'fs';
import crypto from 'crypto';
import path from 'path';
import os from 'os';
import { logger } from 'api/helpers/logger';
import yaml from 'js-yaml';
import AES from 'crypto-js/aes';
import { getUUID } from 'src/utils/auth';
type EncodingType = 'hex' | 'utf-8';
export const readFile = (
filePath: string,
encoding: EncodingType = 'hex'
): string | null => {
if (filePath === '') {
return null;
}
const fileExists = fs.existsSync(filePath);
if (!fileExists) {
logger.error(`No file found at path: ${filePath}`);
return null;
} else {
try {
const file = fs.readFileSync(filePath, encoding);
return file;
} catch (err) {
logger.error('Something went wrong while reading the file: \n' + err);
return null;
}
}
};
type AccountType = {
name: string;
serverUrl: string;
macaroonPath: string;
certificatePath: string;
password: string | null;
};
type AccountConfigType = {
masterPassword: string | null;
accounts: AccountType[];
};
export const parseYaml = (filePath: string): AccountConfigType | null => {
if (filePath === '') {
return null;
}
const yamlConfig = readFile(filePath, 'utf-8');
if (!yamlConfig) {
return null;
}
try {
const yamlObject = yaml.safeLoad(yamlConfig);
return yamlObject;
} catch (err) {
logger.error(
'Something went wrong while parsing the YAML config file: \n' + err
);
return null;
}
};
export const getAccounts = (filePath: string) => {
if (filePath === '') {
logger.verbose('No account config file path provided');
return null;
}
const accountConfig = parseYaml(filePath);
if (!accountConfig) {
logger.info(`No account config file found at path ${filePath}`);
return null;
}
const { masterPassword, accounts } = accountConfig;
if (!accounts || accounts.length <= 0) {
logger.warn(`Account config found at path ${filePath} but had no accounts`);
return null;
}
const readAccounts = [];
const parsedAccounts = accounts
.map((account, index) => {
const {
name,
serverUrl,
macaroonPath,
certificatePath,
password,
} = account;
const missingFields: string[] = [];
if (!name) missingFields.push('name');
if (!serverUrl) missingFields.push('server url');
if (!macaroonPath) missingFields.push('macaroon path');
if (missingFields.length > 0) {
const text = missingFields.join(', ');
logger.error(`Account in index ${index} is missing the fields ${text}`);
return null;
}
if (!password && !masterPassword) {
logger.error(
`You must set a password for account ${name} or set a master password`
);
return null;
}
if (!certificatePath)
logger.warn(
`No certificate for account ${name}. Make sure you don't need it to connect.`
);
const cert = (certificatePath && readFile(certificatePath)) || null;
const clearMacaroon = readFile(macaroonPath);
if (certificatePath && !cert)
logger.warn(
`No certificate for account ${name}. Make sure you don't need it to connect.`
);
if (!clearMacaroon) {
logger.error(`No macarron found for account ${name}.`);
return null;
}
const macaroon = AES.encrypt(
clearMacaroon,
password || masterPassword
).toString();
const id = getUUID(`${name}${serverUrl}${clearMacaroon}${cert}`);
readAccounts.push(name);
return {
name,
id,
host: serverUrl,
macaroon,
cert,
};
})
.filter(Boolean);
const allAccounts = readAccounts.join(', ');
logger.info(`Server accounts that will be available: ${allAccounts}`);
return parsedAccounts;
};
export const readMacaroons = (macaroonPath: string): string | null => {
if (macaroonPath === '') {
logger.verbose('No macaroon path provided');
return null;
}
const adminExists = fs.existsSync(`${macaroonPath}admin.macaroon`);
if (!adminExists) {
logger.error(`No admin.macaroon file found at path: ${macaroonPath}`);
return null;
} else {
try {
const ssoAdmin = fs.readFileSync(`${macaroonPath}admin.macaroon`, 'hex');
return ssoAdmin;
} catch (err) {
logger.error(
'Something went wrong while reading the admin.macaroon: \n' + err
);
return null;
}
}
};
export const createDirectory = (dirname: string) => {
const initDir = path.isAbsolute(dirname) ? path.sep : '';
dirname.split(path.sep).reduce((parentDir, childDir) => {
const curDir = path.resolve(parentDir, childDir);
try {
if (!fs.existsSync(curDir)) {
logger.verbose(`Creating cookie directory at: ${curDir}`);
fs.mkdirSync(curDir);
}
} catch (err) {
if (err.code !== 'EEXIST') {
if (err.code === 'ENOENT') {
throw new Error(
`ENOENT: No such file or directory, mkdir '${dirname}'. Ensure that path separator is '${
os.platform() === 'win32' ? '\\\\' : '/'
}'`
);
} else {
throw err;
}
}
}
return curDir;
}, initDir);
};
export const readCookie = (cookieFile: string): string | null => {
if (cookieFile === '') {
logger.verbose('No cookie path provided');
return null;
}
const exists = fs.existsSync(cookieFile);
if (exists) {
try {
const cookie = fs.readFileSync(cookieFile, 'utf-8');
return cookie;
} catch (err) {
logger.error('Something went wrong while reading cookie: \n' + err);
throw new Error(err);
}
} else {
try {
const dirname = path.dirname(cookieFile);
createDirectory(dirname);
fs.writeFileSync(cookieFile, crypto.randomBytes(64).toString('hex'));
const cookie = fs.readFileSync(cookieFile, 'utf-8');
return cookie;
} catch (err) {
logger.error('Something went wrong while reading the cookie: \n' + err);
throw new Error(err);
}
}
};
export const refreshCookie = (cookieFile: string) => {
try {
logger.verbose('Refreshing cookie for next authentication');
fs.writeFileSync(cookieFile, crypto.randomBytes(64).toString('hex'));
} catch (err) {
logger.error('Something went wrong while refreshing cookie: \n' + err);
throw new Error(err);
}
};

View File

@ -1,10 +1,23 @@
import { authenticatedLndGrpc } from 'ln-service';
import getConfig from 'next/config';
import {
SSO_ACCOUNT,
SERVER_ACCOUNT,
AuthType,
CLIENT_ACCOUNT,
} from 'src/context/AccountContext';
import { ContextType } from 'api/types/apiTypes';
import { logger } from './logger';
const { serverRuntimeConfig } = getConfig();
const { nodeEnv } = serverRuntimeConfig;
type LndAuthType = {
cert: string;
macaroon: string;
host: string;
};
export const getIp = (req: any) => {
if (!req || !req.headers) {
return '';
@ -17,11 +30,33 @@ export const getIp = (req: any) => {
return ip;
};
export const getAuthLnd = (auth: {
cert: string;
macaroon: string;
host: string;
}) => {
export const getCorrectAuth = (
auth: AuthType,
context: ContextType
): LndAuthType => {
if (auth.type === SERVER_ACCOUNT) {
const { account } = context;
if (!account || account.id !== auth.id)
throw new Error('This account is not authenticated');
const foundAccount = context.accounts.find(a => a.id === account.id);
if (!foundAccount) throw new Error('This account does not exist');
return account;
}
if (auth.type === SSO_ACCOUNT) {
if (!context.ssoVerified)
throw new Error('This account is not authenticated');
return { ...context.sso };
}
if (auth.type === CLIENT_ACCOUNT) {
const { host, macaroon, cert } = auth;
return { host, macaroon, cert };
}
throw new Error('This account type does not exist');
};
export const getAuthLnd = (auth: LndAuthType) => {
const cert = auth.cert || '';
const macaroon = auth.macaroon || '';
const socket = auth.host || '';

View File

@ -1,42 +1,39 @@
import path from 'path';
import { createLogger, format, transports } from 'winston';
import getConfig from 'next/config';
const { serverRuntimeConfig } = getConfig();
const { logLevel, nodeEnv } = serverRuntimeConfig;
const level = nodeEnv === 'development' ? 'debug' : logLevel;
const combinedFormat =
nodeEnv === 'development'
? format.combine(
format.label({
label: path.basename(
process && process.mainModule ? process.mainModule.filename : ''
),
}),
format.splat(),
format.colorize(),
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
format.printf(
(info: any) =>
`${info.timestamp} ${info.level} [${info.label}]: ${info.message}`
)
)
: format.combine(
format.label({
label: path.basename(
process && process.mainModule ? process.mainModule.filename : ''
),
}),
format.splat(),
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
format.printf(
(info: any) =>
`${info.timestamp} ${info.level} [${info.label}]: ${info.message}`
)
);
// nodeEnv === 'development' ?
format.combine(
format.label({ label: 'THUB' }),
format.splat(),
format.colorize(),
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
format.printf(
(info: any) =>
`${info.timestamp} ${info.level} [${info.label}]: ${info.message}`
)
);
// : format.combine(
// format.label({
// label: path.basename(
// process && process.mainModule ? process.mainModule.filename : ''
// ),
// }),
// format.splat(),
// format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
// format.printf(
// (info: any) =>
// `${info.timestamp} ${info.level} [${info.label}]: ${info.message}`
// )
// );
export const logger = createLogger({
level: logLevel,
level,
format: combinedFormat,
transports: [new transports.Console()],
});

View File

@ -1,4 +1,5 @@
import { getGraphQLRateLimiter } from 'graphql-rate-limit';
import { logger } from './logger';
interface RateConfigProps {
[key: string]: {
@ -7,7 +8,9 @@ interface RateConfigProps {
};
}
export const RateConfig: RateConfigProps = {};
export const RateConfig: RateConfigProps = {
getMessages: { max: 10, window: '5s' },
};
const rateLimiter = getGraphQLRateLimiter({
identifyContext: (ctx: string) => ctx,
@ -25,5 +28,8 @@ export const requestLimiter = async (rate: string, field: string) => {
},
{ max, window }
);
if (errorMessage) throw new Error(errorMessage);
if (errorMessage) {
logger.warn(`Rate limit reached for '${field}' from ip ${rate}`);
throw new Error(errorMessage);
}
};

View File

@ -5,9 +5,14 @@ import {
GraphQLInt,
GraphQLNonNull,
} from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { CloseChannelType } from '../../types/MutationType';
@ -25,10 +30,11 @@ export const closeChannel = {
targetConfirmations: { type: GraphQLInt },
tokensPerVByte: { type: GraphQLInt },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'closeChannel');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const info: CloseChannelProps = await lnCloseChannel({

View File

@ -5,9 +5,14 @@ import {
GraphQLInt,
GraphQLNonNull,
} from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { OpenChannelType } from '../../types/MutationType';
@ -25,10 +30,11 @@ export const openChannel = {
tokensPerVByte: { type: GraphQLInt },
isPrivate: { type: GraphQLBoolean },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'openChannel');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const info: OpenChannelProps = await lnOpenChannel({

View File

@ -1,8 +1,14 @@
import { updateRoutingFees } from 'ln-service';
import { GraphQLBoolean, GraphQLString, GraphQLInt } from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import {
getErrorMsg,
getAuthLnd,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
export const updateFees = {
@ -14,11 +20,12 @@ export const updateFees = {
baseFee: { type: GraphQLInt },
feeRate: { type: GraphQLInt },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'updateFees');
const { auth, transactionId, transactionVout, baseFee, feeRate } = params;
const { transactionId, transactionVout, baseFee, feeRate } = params;
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
if (!baseFee && !feeRate) {

View File

@ -6,8 +6,9 @@ import {
signMessage,
} from 'ln-service';
import { GraphQLString, GraphQLNonNull, GraphQLInt } from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, to } from '../../../helpers/helpers';
import { getAuthLnd, to, getCorrectAuth } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { createCustomRecords } from '../../../helpers/customRecords';
@ -21,9 +22,11 @@ export const sendMessage = {
tokens: { type: GraphQLInt },
maxFee: { type: GraphQLInt },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'sendMessage');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
if (params.maxFee) {
const tokens = Math.max(params.tokens || 100, 100);

View File

@ -1,8 +1,13 @@
import { createInvoice as createInvoiceRequest } from 'ln-service';
import { GraphQLNonNull, GraphQLInt } from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { InvoiceType } from '../../types/MutationType';
@ -22,10 +27,11 @@ export const createInvoice = {
...defaultParams,
amount: { type: new GraphQLNonNull(GraphQLInt) },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'createInvoice');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const invoice: InvoiceProps = await createInvoiceRequest({

View File

@ -1,8 +1,13 @@
import { parsePaymentRequest } from 'ln-service';
import { GraphQLString, GraphQLNonNull } from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { ParsePaymentType } from '../../types/MutationType';
@ -36,10 +41,11 @@ export const parsePayment = {
...defaultParams,
request: { type: new GraphQLNonNull(GraphQLString) },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'parsePayment');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const request: RequestProps = await parsePaymentRequest({

View File

@ -5,9 +5,14 @@ import {
payViaPaymentDetails,
} from 'ln-service';
import { GraphQLString, GraphQLNonNull, GraphQLInt } from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { PayType } from '../../types/MutationType';
@ -55,9 +60,11 @@ export const pay = {
request: { type: new GraphQLNonNull(GraphQLString) },
tokens: { type: GraphQLInt },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'pay');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
let isRequest = false;
try {

View File

@ -1,8 +1,13 @@
import { GraphQLNonNull, GraphQLBoolean, GraphQLString } from 'graphql';
import { payViaRoutes, createInvoice } from 'ln-service';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
export const payViaRoute = {
@ -11,10 +16,11 @@ export const payViaRoute = {
...defaultParams,
route: { type: new GraphQLNonNull(GraphQLString) },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'payViaRoute');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
let route;
try {

View File

@ -1,8 +1,13 @@
import { createChainAddress } from 'ln-service';
import { GraphQLString, GraphQLBoolean } from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
interface AddressProps {
@ -15,10 +20,11 @@ export const createAddress = {
...defaultParams,
nested: { type: GraphQLBoolean },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'getAddress');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
const format = params.nested ? 'np2wpkh' : 'p2wpkh';

View File

@ -5,9 +5,14 @@ import {
GraphQLBoolean,
GraphQLInt,
} from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { SendToType } from '../../types/MutationType';
@ -29,10 +34,11 @@ export const sendToAddress = {
target: { type: GraphQLInt },
sendAll: { type: GraphQLBoolean },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'sendToAddress');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
const props = params.fee
? { fee_tokens_per_vbyte: params.fee }

View File

@ -1,8 +1,13 @@
import { addPeer as addLnPeer } from 'ln-service';
import { GraphQLBoolean, GraphQLString, GraphQLNonNull } from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
export const addPeer = {
@ -13,10 +18,11 @@ export const addPeer = {
socket: { type: new GraphQLNonNull(GraphQLString) },
isTemporary: { type: GraphQLBoolean },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'addPeer');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const success: boolean = await addLnPeer({

View File

@ -1,8 +1,13 @@
import { removePeer as removeLnPeer } from 'ln-service';
import { GraphQLBoolean, GraphQLString, GraphQLNonNull } from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
export const removePeer = {
@ -11,10 +16,11 @@ export const removePeer = {
...defaultParams,
publicKey: { type: new GraphQLNonNull(GraphQLString) },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'removePeer');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const success: boolean = await removeLnPeer({

View File

@ -0,0 +1,42 @@
import { GraphQLString } from 'graphql';
import getConfig from 'next/config';
import jwt from 'jsonwebtoken';
import { readCookie, refreshCookie } from 'api/helpers/fileHelpers';
import { ContextType } from 'api/types/apiTypes';
import { SSO_ACCOUNT } from 'src/context/AccountContext';
import { logger } from 'api/helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
const { serverRuntimeConfig } = getConfig();
const { cookiePath } = serverRuntimeConfig;
export const getAuthToken = {
type: GraphQLString,
args: {
cookie: { type: GraphQLString },
},
resolve: async (_: undefined, params: any, context: ContextType) => {
const { ip, secret } = context;
await requestLimiter(ip, 'getAuthToken');
if (!params.cookie) {
return null;
}
if (cookiePath === '') {
logger.warn('SSO auth not available since no cookie path was provided');
return null;
}
const cookieFile = readCookie(cookiePath);
refreshCookie(cookiePath);
if (cookieFile === params.cookie) {
const token = jwt.sign({ user: SSO_ACCOUNT }, secret);
return token;
}
return null;
},
};

View File

@ -0,0 +1,39 @@
import { GraphQLString } from 'graphql';
import jwt from 'jsonwebtoken';
import CryptoJS from 'crypto-js';
import { ContextType } from 'api/types/apiTypes';
import AES from 'crypto-js/aes';
import { requestLimiter } from '../../../helpers/rateLimiter';
export const getSessionToken = {
type: GraphQLString,
args: {
id: { type: GraphQLString },
password: { type: GraphQLString },
},
resolve: async (_: undefined, params: any, context: ContextType) => {
const { ip, secret } = context;
await requestLimiter(ip, 'getSessionToken');
const account = context.accounts.find(a => a.id === params.id) || null;
if (!account) return null;
try {
const bytes = AES.decrypt(account.macaroon, params.password);
const decrypted = bytes.toString(CryptoJS.enc.Utf8);
const token = jwt.sign(
{
id: params.id,
macaroon: decrypted,
cert: account.cert,
host: account.host,
},
secret
);
return AES.encrypt(token, secret).toString();
} catch (error) {
throw new Error('Wrong password');
}
},
};

View File

@ -0,0 +1,7 @@
import { getAuthToken } from './getAuthToken';
import { getSessionToken } from './getSessionToken';
export const authTokenQueries = {
getAuthToken,
getSessionToken,
};

View File

@ -1,17 +1,23 @@
import { getBackups as getLnBackups } from 'ln-service';
import { GraphQLString } from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
export const getBackups = {
type: GraphQLString,
args: defaultParams,
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'getBackups');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const backups = await getLnBackups({

View File

@ -1,8 +1,13 @@
import { recoverFundsFromChannels } from 'ln-service';
import { GraphQLNonNull, GraphQLString, GraphQLBoolean } from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
interface BackupProps {
@ -15,10 +20,11 @@ export const recoverFunds = {
...defaultParams,
backup: { type: new GraphQLNonNull(GraphQLString) },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'recoverFunds');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
let backupObj: BackupProps = { backup: '' };
try {

View File

@ -1,8 +1,13 @@
import { verifyBackups as verifyLnBackups } from 'ln-service';
import { GraphQLNonNull, GraphQLString, GraphQLBoolean } from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
interface BackupProps {
@ -16,10 +21,11 @@ export const verifyBackups = {
...defaultParams,
backup: { type: new GraphQLNonNull(GraphQLString) },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'verifyBackups');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
let backupObj: BackupProps = { backup: '', channels: [] };
try {

View File

@ -3,9 +3,14 @@ import {
getPendingChainBalance as getPending,
} from 'ln-service';
import { GraphQLInt } from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
interface ChainBalanceProps {
@ -19,10 +24,11 @@ interface PendingChainBalanceProps {
export const getChainBalance = {
type: GraphQLInt,
args: defaultParams,
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'chainBalance');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const value: ChainBalanceProps = await getBalance({
@ -39,10 +45,11 @@ export const getChainBalance = {
export const getPendingChainBalance = {
type: GraphQLInt,
args: defaultParams,
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'pendingChainBalance');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const pendingValue: PendingChainBalanceProps = await getPending({

View File

@ -1,9 +1,14 @@
import { GraphQLList } from 'graphql';
import { getChainTransactions as getLnChainTransactions } from 'ln-service';
import { sortBy } from 'underscore';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { GetChainTransactionsType } from '../../types/QueryType';
@ -25,10 +30,11 @@ interface TransactionsProps {
export const getChainTransactions = {
type: new GraphQLList(GetChainTransactionsType),
args: defaultParams,
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'chainTransactions');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const transactionList: TransactionsProps = await getLnChainTransactions({

View File

@ -5,9 +5,14 @@ import {
GraphQLString,
GraphQLList,
} from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
const GetUtxosType = new GraphQLObjectType({
@ -26,10 +31,11 @@ const GetUtxosType = new GraphQLObjectType({
export const getUtxos = {
type: new GraphQLList(GetUtxosType),
args: defaultParams,
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'getUtxos');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const { utxos } = await getLnUtxos({ lnd });

View File

@ -1,7 +1,12 @@
import { getChannelBalance as getLnChannelBalance } from 'ln-service';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { ChannelBalanceType } from '../../types/QueryType';
@ -13,10 +18,11 @@ interface ChannelBalanceProps {
export const getChannelBalance = {
type: ChannelBalanceType,
args: defaultParams,
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'channelBalance');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const channelBalance: ChannelBalanceProps = await getLnChannelBalance({

View File

@ -1,8 +1,13 @@
import { getFeeRates, getChannels, getNode } from 'ln-service';
import { GraphQLList } from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { ChannelFeeType } from '../../types/QueryType';
@ -34,10 +39,11 @@ interface NodeProps {
export const getChannelFees = {
type: new GraphQLList(ChannelFeeType),
args: defaultParams,
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'channelFees');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const channels: GetChannelsProps = await getChannels({ lnd });

View File

@ -1,7 +1,12 @@
import { getChannels } from 'ln-service';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { ChannelReportType } from '../../types/QueryType';
@ -17,10 +22,11 @@ interface ChannelsProps {
export const getChannelReport = {
type: ChannelReportType,
args: defaultParams,
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'channelReport');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const channels: GetChannelsProps = await getChannels({ lnd });

View File

@ -1,8 +1,13 @@
import { GraphQLList, GraphQLBoolean } from 'graphql';
import { getChannels as getLnChannels, getNode } from 'ln-service';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { ChannelType } from '../../types/QueryType';
@ -42,10 +47,11 @@ export const getChannels = {
...defaultParams,
active: { type: GraphQLBoolean },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'channels');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const channelList: ChannelListProps = await getLnChannels({

View File

@ -1,8 +1,13 @@
import { GraphQLList, GraphQLString } from 'graphql';
import { getClosedChannels as getLnClosedChannels, getNode } from 'ln-service';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { ClosedChannelType } from '../../types/QueryType';
@ -34,10 +39,11 @@ export const getClosedChannels = {
...defaultParams,
type: { type: GraphQLString },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'closedChannels');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const closedChannels: ChannelListProps = await getLnClosedChannels({

View File

@ -3,9 +3,14 @@ import {
getNode,
} from 'ln-service';
import { GraphQLList } from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { PendingChannelType } from '../../types/QueryType';
@ -33,10 +38,11 @@ interface PendingChannelProps {
export const getPendingChannels = {
type: new GraphQLList(PendingChannelType),
args: defaultParams,
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'pendingChannels');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const pendingChannels: PendingChannelListProps = await getLnPendingChannels(

View File

@ -1,7 +1,8 @@
import { GraphQLString, GraphQLBoolean } from 'graphql';
import { getInvoices, verifyMessage } from 'ln-service';
import { ContextType } from 'api/types/apiTypes';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, to } from '../../../helpers/helpers';
import { getAuthLnd, to, getCorrectAuth } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { decodeMessage } from '../../../helpers/customRecords';
import { GetMessagesType } from '../../types/QueryType';
@ -14,10 +15,11 @@ export const getMessages = {
initialize: { type: GraphQLBoolean },
lastMessage: { type: GraphQLString },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'getMessages');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
const invoiceList = await to(
getInvoices({

View File

@ -1,5 +1,6 @@
import { GraphQLBoolean } from 'graphql';
import fetch from 'node-fetch';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { BitcoinFeeType } from '../../types/QueryType';
@ -10,7 +11,7 @@ export const getBitcoinFees = {
args: {
logger: { type: GraphQLBoolean },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'bitcoinFee');
try {

View File

@ -1,5 +1,6 @@
import { GraphQLString, GraphQLBoolean } from 'graphql';
import fetch from 'node-fetch';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { appUrls } from '../../../utils/appUrls';
@ -12,7 +13,7 @@ export const getBitcoinPrice = {
type: GraphQLString,
},
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'bitcoinPrice');
try {

View File

@ -5,9 +5,14 @@ import {
} from 'ln-service';
import { differenceInHours, differenceInCalendarDays } from 'date-fns';
import { groupBy } from 'underscore';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { reduceInOutArray } from '../report/Helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { InOutType } from '../../types/QueryType';
@ -19,10 +24,11 @@ export const getInOut = {
...defaultParams,
time: { type: GraphQLString },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'getInOut');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
const endDate = new Date();
let periods = 7;

View File

@ -1,7 +1,12 @@
import { pay as payRequest } from 'ln-service';
import { GraphQLBoolean } from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { logger } from '../../../helpers/logger';
@ -10,10 +15,11 @@ export const adminCheck = {
args: {
...defaultParams,
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'adminCheck');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
await payRequest({

View File

@ -1,8 +1,13 @@
import { decodePaymentRequest } from 'ln-service';
import { GraphQLString, GraphQLNonNull } from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { DecodeType } from '../../types/QueryType';
@ -32,10 +37,11 @@ export const decodeRequest = {
...defaultParams,
request: { type: new GraphQLNonNull(GraphQLString) },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'decode');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const decode: DecodeProps = await decodePaymentRequest({

View File

@ -1,8 +1,13 @@
import { GraphQLString, GraphQLNonNull, GraphQLBoolean } from 'graphql';
import { getNode as getLnNode } from 'ln-service';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { PartnerNodeType } from '../../types/QueryType';
@ -14,10 +19,11 @@ export const getNode = {
publicKey: { type: new GraphQLNonNull(GraphQLString) },
withoutChannels: { type: GraphQLBoolean },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'closedChannels');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const nodeInfo = await getLnNode({

View File

@ -1,7 +1,12 @@
import { getNetworkInfo as getLnNetworkInfo } from 'ln-service';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { NetworkInfoType } from '../../types/QueryType';
@ -20,10 +25,11 @@ interface NetworkInfoProps {
export const getNetworkInfo = {
type: NetworkInfoType,
args: defaultParams,
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'networkInfo');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const info: NetworkInfoProps = await getLnNetworkInfo({ lnd });

View File

@ -1,7 +1,12 @@
import { getWalletInfo, getClosedChannels } from 'ln-service';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { NodeInfoType } from '../../types/QueryType';
@ -25,10 +30,11 @@ interface NodeInfoProps {
export const getNodeInfo = {
type: NodeInfoType,
args: defaultParams,
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'nodeInfo');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const info: NodeInfoProps = await getWalletInfo({

View File

@ -1,16 +1,18 @@
import { getWalletVersion } from 'ln-service';
import { ContextType } from 'api/types/apiTypes';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, to } from '../../../helpers/helpers';
import { getAuthLnd, to, getCorrectAuth } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { WalletInfoType } from '../../types/QueryType';
export const getWalletInfo = {
type: WalletInfoType,
args: defaultParams,
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'getWalletInfo');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
return await to(
getWalletVersion({

View File

@ -1,6 +1,7 @@
import fetch from 'node-fetch';
import { GraphQLList } from 'graphql';
import getConfig from 'next/config';
import { ContextType } from 'api/types/apiTypes';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { logger } from '../../../helpers/logger';
import { appUrls } from '../../../utils/appUrls';
@ -12,7 +13,7 @@ const { hodlKey } = serverRuntimeConfig;
export const getCountries = {
type: new GraphQLList(HodlCountryType),
args: {},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'getCountries');
const headers = {

View File

@ -1,6 +1,7 @@
import fetch from 'node-fetch';
import { GraphQLList } from 'graphql';
import getConfig from 'next/config';
import { ContextType } from 'api/types/apiTypes';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { logger } from '../../../helpers/logger';
import { appUrls } from '../../../utils/appUrls';
@ -12,7 +13,7 @@ const { hodlKey } = serverRuntimeConfig;
export const getCurrencies = {
type: new GraphQLList(HodlCurrencyType),
args: {},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'getCurrencies');
const headers = {

View File

@ -1,4 +1,5 @@
import { GraphQLList, GraphQLString } from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { logger } from '../../../helpers/logger';
import { appUrls } from '../../../utils/appUrls';
@ -18,7 +19,7 @@ export const getOffers = {
args: {
filter: { type: GraphQLString },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'getOffers');
let queryParams = defaultQuery;

View File

@ -11,6 +11,8 @@ import { messageQueries } from './message';
import { chainQueries } from './chain';
import { hodlQueries } from './hodlhodl';
import { chatQueries } from './chat';
import { authTokenQueries } from './authToken';
import { serverQueries } from './server';
export const query = {
...channelQueries,
@ -26,4 +28,6 @@ export const query = {
...chainQueries,
...hodlQueries,
...chatQueries,
...authTokenQueries,
...serverQueries,
};

View File

@ -1,8 +1,13 @@
import { signMessage as signLnMessage } from 'ln-service';
import { GraphQLString, GraphQLNonNull } from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { defaultParams } from '../../../helpers/defaultProps';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { logger } from '../../../helpers/logger';
export const signMessage = {
@ -11,10 +16,11 @@ export const signMessage = {
...defaultParams,
message: { type: new GraphQLNonNull(GraphQLString) },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'signMessage');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const message: { signature: string } = await signLnMessage({

View File

@ -1,8 +1,13 @@
import { verifyMessage as verifyLnMessage } from 'ln-service';
import { GraphQLString, GraphQLNonNull } from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { defaultParams } from '../../../helpers/defaultProps';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { logger } from '../../../helpers/logger';
export const verifyMessage = {
@ -12,10 +17,11 @@ export const verifyMessage = {
message: { type: new GraphQLNonNull(GraphQLString) },
signature: { type: new GraphQLNonNull(GraphQLString) },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'verifyMessage');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const message: { signed_by: string } = await verifyLnMessage({

View File

@ -1,8 +1,13 @@
import { GraphQLList } from 'graphql';
import { getPeers as getLnPeers, getNode } from 'ln-service';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { PeerType } from '../../types/QueryType';
@ -22,10 +27,11 @@ interface PeerProps {
export const getPeers = {
type: new GraphQLList(PeerType),
args: defaultParams,
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'getPeers');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
try {
const { peers }: { peers: PeerProps[] } = await getLnPeers({

View File

@ -7,9 +7,14 @@ import {
} from 'ln-service';
import { subHours, subDays } from 'date-fns';
import { sortBy } from 'underscore';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { countArray, countRoutes } from './Helpers';
import { ForwardCompleteProps } from './ForwardReport.interface';
@ -31,10 +36,11 @@ export const getForwardChannelsReport = {
order: { type: GraphQLString },
type: { type: GraphQLString },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'forwardChannels');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
let startDate = new Date();
const endDate = new Date();

View File

@ -7,9 +7,14 @@ import {
differenceInHours,
differenceInCalendarDays,
} from 'date-fns';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { reduceForwardArray } from './Helpers';
import { ForwardCompleteProps } from './ForwardReport.interface';
@ -20,10 +25,11 @@ export const getForwardReport = {
...defaultParams,
time: { type: GraphQLString },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'forwardReport');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
let startDate = new Date();
const endDate = new Date();

View File

@ -1,8 +1,13 @@
import { GraphQLNonNull, GraphQLString, GraphQLInt } from 'graphql';
import { getRouteToDestination, getWalletInfo } from 'ln-service';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
export const getRoutes = {
@ -14,10 +19,11 @@ export const getRoutes = {
tokens: { type: new GraphQLNonNull(GraphQLInt) },
maxFee: { type: GraphQLInt },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'getRoutes');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
const { public_key } = await getWalletInfo({ lnd });

View File

@ -0,0 +1,32 @@
import { GraphQLList } from 'graphql';
import { ContextType } from 'api/types/apiTypes';
import { ServerAccountType } from 'api/schemas/types/QueryType';
import { SSO_ACCOUNT } from 'src/context/AccountContext';
import { requestLimiter } from '../../../helpers/rateLimiter';
export const getServerAccounts = {
type: new GraphQLList(ServerAccountType),
resolve: async (_: undefined, params: any, context: ContextType) => {
const { ip, accounts, account, sso, ssoVerified } = context;
await requestLimiter(ip, 'getServerAccounts');
const { macaroon, cert, host } = sso;
let ssoAccount = null;
if (macaroon && cert && host && ssoVerified) {
ssoAccount = {
name: 'SSO Account',
id: SSO_ACCOUNT,
loggedIn: true,
};
}
const currentId = account?.id;
const withStatus =
accounts?.map(a => ({
...a,
loggedIn: a.id === currentId,
})) || [];
return ssoAccount ? [...withStatus, ssoAccount] : withStatus;
},
};

View File

@ -0,0 +1,5 @@
import { getServerAccounts } from './getServerAccounts';
export const serverQueries = {
getServerAccounts,
};

View File

@ -7,9 +7,14 @@ import {
} from 'ln-service';
import { sortBy } from 'underscore';
import { subHours, subDays, subMonths, subYears } from 'date-fns';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { ForwardCompleteProps } from '../report/ForwardReport.interface';
import { defaultParams } from '../../../helpers/defaultProps';
import { GetForwardType } from '../../types/QueryType';
@ -29,10 +34,11 @@ export const getForwards = {
...defaultParams,
time: { type: GraphQLString },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'forwards');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
let startDate = new Date();
const endDate = new Date();

View File

@ -2,9 +2,14 @@ import { GraphQLString } from 'graphql';
import { getPayments, getInvoices, getNode } from 'ln-service';
import { compareDesc } from 'date-fns';
import { sortBy } from 'underscore';
import { ContextType } from 'api/types/apiTypes';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import {
getAuthLnd,
getErrorMsg,
getCorrectAuth,
} from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { GetResumeType } from '../../types/QueryType';
import { PaymentsProps, InvoicesProps, NodeProps } from './resume.interface';
@ -15,10 +20,11 @@ export const getResume = {
...defaultParams,
token: { type: GraphQLString },
},
resolve: async (root: any, params: any, context: any) => {
resolve: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'payments');
const lnd = getAuthLnd(params.auth);
const auth = getCorrectAuth(params.auth, context);
const lnd = getAuthLnd(auth);
let payments;
let invoices;

View File

@ -1,9 +1,15 @@
import { GraphQLInputObjectType, GraphQLString } from 'graphql';
import { GraphQLInputObjectType, GraphQLString, GraphQLNonNull } from 'graphql';
export const AuthType = new GraphQLInputObjectType({
name: 'authType',
fields: () => {
return {
type: {
type: new GraphQLNonNull(GraphQLString),
},
id: {
type: GraphQLString,
},
host: {
type: GraphQLString,
},

View File

@ -4,6 +4,7 @@ import {
GraphQLInt,
GraphQLBoolean,
GraphQLList,
GraphQLNonNull,
} from 'graphql';
export const ChannelBalanceType = new GraphQLObjectType({
@ -328,3 +329,12 @@ export const GetMessagesType = new GraphQLObjectType({
messages: { type: new GraphQLList(MessagesType) },
}),
});
export const ServerAccountType = new GraphQLObjectType({
name: 'serverAccountType',
fields: () => ({
name: { type: new GraphQLNonNull(GraphQLString) },
id: { type: new GraphQLNonNull(GraphQLString) },
loggedIn: { type: new GraphQLNonNull(GraphQLBoolean) },
}),
});

26
api/types/apiTypes.ts Normal file
View File

@ -0,0 +1,26 @@
type SSOType = {
macaroon: string | null;
cert: string | null;
host: string | null;
};
type VerifiedAccountType = {
id: string;
} & SSOType;
type AccountType = {
name: string;
id: string;
host: string;
macaroon: string;
cert: string | null;
};
export type ContextType = {
ip: string;
secret: string;
ssoVerified: boolean;
account: VerifiedAccountType | null;
sso: SSOType;
accounts: AccountType[];
};

View File

@ -2,12 +2,18 @@ overwrite: true
schema: 'http://localhost:3000/api/v1'
documents: 'src/graphql/**/*.ts'
generates:
src/generated/graphql.tsx:
src/graphql/types.ts:
- typescript
src/graphql/:
preset: near-operation-file
presetConfig:
baseTypesPath: types.ts
extension: .generated.tsx
folder: __generated__
config:
withComponent: false
withHOC: false
withHooks: true
plugins:
- 'typescript'
- 'typescript-operations'
- 'typescript-react-apollo'

View File

@ -8,8 +8,13 @@ module.exports = withBundleAnalyzer({
assetPrefix: process.env.BASE_PATH || '',
serverRuntimeConfig: {
nodeEnv: process.env.NODE_ENV || 'development',
logLevel: process.env.LOG_LEVEL || 'silly',
logLevel: process.env.LOG_LEVEL || 'info',
hodlKey: process.env.HODL_KEY || '',
cookiePath: process.env.COOKIE_PATH || '',
lnServerUrl: process.env.SSO_SERVER_URL || '',
lnCertPath: process.env.SSO_CERT_PATH || '',
macaroonPath: process.env.SSO_MACAROON_PATH || '',
accountConfigPath: process.env.ACCOUNT_CONFIG_PATH || '',
},
publicRuntimeConfig: {
nodeEnv: process.env.NODE_ENV || 'development',

View File

@ -4,6 +4,7 @@
"description": "",
"main": "index.js",
"scripts": {
"bs": "yarn build && yarn start",
"dev": "cross-env NODE_OPTIONS='--insecure-http-parser' next",
"dev:compatible": "next",
"build": "next build",
@ -25,31 +26,35 @@
"build:32": "docker build --no-cache -f arm32v7.Dockerfile -t apotdevin/thunderhub:test-arm32v7 .",
"build:64": "docker build -f arm64v8.Dockerfile -t apotdevin/thunderhub:test-arm64v8 .",
"build:manifest": "docker manifest create apotdevin/thunderhub:test apotdevin/thunderhub:test-amd64 apotdevin/thunderhub:test-arm32v7 apotdevin/thunderhub:test-arm64v8",
"upgrade-latest": "yarn upgrade-interactive --latest"
"upgrade-latest": "yarn upgrade-interactive --latest",
"tsc": "tsc"
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"@apollo/react-hooks": "^3.1.5",
"apollo-boost": "^0.4.8",
"@graphql-codegen/near-operation-file-preset": "^1.13.5",
"apollo-boost": "^0.4.9",
"apollo-server-micro": "^2.13.1",
"base64url": "^3.0.1",
"cookie": "^0.4.1",
"crypto-js": "^4.0.0",
"date-fns": "^2.13.0",
"date-fns": "^2.14.0",
"graphql": "^15.0.0",
"graphql-iso-date": "^3.6.1",
"graphql-rate-limit": "^2.0.1",
"graphql-tag": "^2.10.3",
"intersection-observer": "^0.10.0",
"js-cookie": "^2.2.1",
"ln-service": "^48.0.5",
"js-yaml": "^3.13.1",
"jsonwebtoken": "^8.5.1",
"ln-service": "^48.1.0",
"lodash.debounce": "^4.0.8",
"lodash.groupby": "^4.6.0",
"lodash.merge": "^4.6.2",
"micro-cors": "^0.1.1",
"next": "^9.4.0",
"next": "^9.4.1",
"next-with-apollo": "^5.0.1",
"numeral": "^2.0.6",
"qrcode.react": "^1.0.0",
@ -61,14 +66,14 @@
"react-qr-reader": "^2.2.1",
"react-spinners": "^0.8.3",
"react-spring": "^8.0.27",
"react-toastify": "^6.0.1",
"react-toastify": "^6.0.4",
"react-tooltip": "^4.2.6",
"styled-components": "^5.1.0",
"styled-react-modal": "^2.0.1",
"styled-theming": "^2.2.0",
"underscore": "^1.10.2",
"uuid": "^8.0.0",
"victory": "^34.2.2",
"victory": "^34.3.6",
"winston": "^3.2.1",
"zxcvbn": "^4.4.2"
},
@ -82,19 +87,20 @@
"@graphql-codegen/typescript-operations": "^1.13.5",
"@graphql-codegen/typescript-react-apollo": "1.13.5",
"@graphql-codegen/typescript-resolvers": "1.13.5",
"@next/bundle-analyzer": "^9.4.0",
"@next/bundle-analyzer": "^9.4.1",
"@storybook/addon-actions": "^5.3.18",
"@storybook/addon-knobs": "^5.3.18",
"@storybook/addon-viewport": "^5.3.18",
"@storybook/react": "^5.3.18",
"@testing-library/jest-dom": "^5.7.0",
"@testing-library/react": "^10.0.4",
"@types/node": "^13.13.5",
"@types/jsonwebtoken": "^8.5.0",
"@types/node": "^14.0.1",
"@types/react": "^16.9.35",
"@types/styled-components": "^5.1.0",
"@types/styled-theming": "^2.2.2",
"@typescript-eslint/eslint-plugin": "^2.32.0",
"@typescript-eslint/parser": "^2.32.0",
"@types/styled-theming": "^2.2.3",
"@typescript-eslint/eslint-plugin": "^2.34.0",
"@typescript-eslint/parser": "^2.34.0",
"babel-jest": "^26.0.1",
"babel-loader": "^8.1.0",
"babel-plugin-inline-react-svg": "^1.1.1",
@ -105,18 +111,18 @@
"eslint": "^7.0.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-jest": "^23.10.0",
"eslint-plugin-jest": "^23.13.1",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.19.0",
"eslint-plugin-react-hooks": "^4.0.0",
"eslint-plugin-react": "^7.20.0",
"eslint-plugin-react-hooks": "^4.0.2",
"fast-diff": "^1.2.0",
"husky": "^4.2.5",
"jest": "^26.0.1",
"lint-staged": "^10.2.2",
"lint-staged": "^10.2.4",
"prettier": "^2.0.5",
"standard-version": "^8.0.0",
"typescript": "^3.8.3"
"typescript": "^3.9.2"
},
"husky": {
"hooks": {

View File

@ -5,6 +5,12 @@ import { ApolloProvider } from '@apollo/react-hooks';
import { useRouter } from 'next/router';
import { toast } from 'react-toastify';
import Head from 'next/head';
import { AuthSSOCheck } from 'src/components/accounts/AuthSSOCheck';
import { getUrlParam } from 'src/utils/url';
import { ChatInit } from 'src/components/chat/ChatInit';
import { ServerAccounts } from 'src/components/accounts/ServerAccounts';
import { useAccountState } from 'src/context/AccountContext';
import { LoadingCard } from 'src/components/loading/LoadingCard';
import { ContextProvider } from '../src/context/ContextProvider';
import { useConfigState, ConfigProvider } from '../src/context/ConfigContext';
import { GlobalStyles } from '../src/styles/GlobalStyle';
@ -18,7 +24,6 @@ import 'react-toastify/dist/ReactToastify.css';
import { PageWrapper, HeaderBodyWrapper } from '../src/layouts/Layout.styled';
import { useStatusState } from '../src/context/StatusContext';
import { ChatFetcher } from '../src/components/chat/ChatFetcher';
import { ChatInit } from '../src/components/chat/ChatInit';
import { parseCookies } from '../src/utils/cookies';
toast.configure({ draggable: false, pauseOnFocusLoss: false });
@ -27,6 +32,7 @@ const Wrapper: React.FC = ({ children }) => {
const { theme } = useConfigState();
const { pathname } = useRouter();
const { connected } = useStatusState();
const { hasAccount } = useAccountState();
const isRoot = pathname === '/';
@ -34,6 +40,9 @@ const Wrapper: React.FC = ({ children }) => {
if (isRoot) {
return <>{children}</>;
}
if (hasAccount === 'false') {
return <LoadingCard noCard={true} />;
}
return <GridWrapper>{children}</GridWrapper>;
};
@ -63,7 +72,13 @@ const Wrapper: React.FC = ({ children }) => {
);
};
const App = ({ Component, pageProps, apollo, initialConfig }: any) => (
const App = ({
Component,
pageProps,
apollo,
initialConfig,
cookieParam,
}: any) => (
<>
<Head>
<title>ThunderHub - Lightning Node Manager</title>
@ -71,6 +86,8 @@ const App = ({ Component, pageProps, apollo, initialConfig }: any) => (
<ApolloProvider client={apollo}>
<ConfigProvider initialConfig={initialConfig}>
<ContextProvider>
<AuthSSOCheck cookieParam={cookieParam} />
<ServerAccounts />
<Wrapper>
<Component {...pageProps} />
</Wrapper>
@ -81,17 +98,21 @@ const App = ({ Component, pageProps, apollo, initialConfig }: any) => (
);
App.getInitialProps = async props => {
const cookieParam = getUrlParam(props.router?.query?.token);
const cookies = parseCookies(props.ctx.req);
const defaultState = {};
if (!cookies?.config) {
return { initialConfig: {} };
return { initialConfig: {}, ...defaultState };
}
try {
const config = JSON.parse(cookies.config);
return {
initialConfig: config,
initialConfig: { ...config, ...defaultState },
cookieParam,
};
} catch (error) {
return { initialConfig: {} };
return { initialConfig: {}, cookieParam, ...defaultState };
}
};

View File

@ -1,21 +1,94 @@
import crypto from 'crypto';
import { ApolloServer } from 'apollo-server-micro';
import { thunderHubSchema } from 'api/schemas';
import { getIp } from 'api/helpers/helpers';
import getConfig from 'next/config';
import Cors from 'micro-cors';
import jwt from 'jsonwebtoken';
import { logger } from 'api/helpers/logger';
import {
readMacaroons,
readFile,
readCookie,
getAccounts,
} from 'api/helpers/fileHelpers';
import { ContextType } from 'api/types/apiTypes';
import AES from 'crypto-js/aes';
import CryptoJS from 'crypto-js';
const { publicRuntimeConfig } = getConfig();
const { apiBaseUrl } = publicRuntimeConfig;
const { publicRuntimeConfig, serverRuntimeConfig } = getConfig();
const { apiBaseUrl, nodeEnv } = publicRuntimeConfig;
const {
cookiePath,
macaroonPath,
lnCertPath,
lnServerUrl,
accountConfigPath,
} = serverRuntimeConfig;
const secret =
nodeEnv === 'development'
? '123456789'
: crypto.randomBytes(64).toString('hex');
const ssoMacaroon = readMacaroons(macaroonPath);
const ssoCert = readFile(lnCertPath);
const accountConfig = getAccounts(accountConfigPath);
readCookie(cookiePath);
const cors = Cors({
origin: true,
allowCredentials: true,
});
const apolloServer = new ApolloServer({
schema: thunderHubSchema,
context: async ({ req }: any) => {
context: async ({ req }) => {
const ip = getIp(req);
return { ip };
let ssoVerified = false;
if (req?.cookies?.SSOAuth) {
try {
jwt.verify(req.cookies.SSOAuth, secret);
ssoVerified = true;
} catch (error) {
logger.verbose('SSO authentication cookie failed');
}
}
let account = null;
if (req?.cookies?.AccountAuth) {
try {
const bytes = AES.decrypt(req.cookies.AccountAuth, secret);
const decrypted = bytes.toString(CryptoJS.enc.Utf8);
const cookieAccount = jwt.verify(decrypted, secret);
const accountId = cookieAccount['id'] || '';
const accountMacaroon = cookieAccount['macaroon'] || '';
const accountCert = cookieAccount['cert'] || '';
const accountHost = cookieAccount['host'] || '';
account = {
id: accountId,
host: accountHost,
cert: accountCert,
macaroon: accountMacaroon,
};
} catch (error) {
logger.verbose('Account authentication cookie failed');
}
}
const context: ContextType = {
ip,
secret,
ssoVerified,
account,
sso: { macaroon: ssoMacaroon, cert: ssoCert, host: lnServerUrl || null },
accounts: accountConfig,
};
return context;
},
});

View File

@ -1,6 +1,8 @@
import React, { useState } from 'react';
import { toast } from 'react-toastify';
import sortBy from 'lodash.sortby';
import { useAccountState } from 'src/context/AccountContext';
import { useGetChannelsQuery } from 'src/graphql/queries/__generated__/getChannels.generated';
import {
CardWithTitle,
Card,
@ -11,7 +13,6 @@ import {
NoWrapTitle,
SingleLine,
} from '../src/components/generic/Styled';
import { useAccount } from '../src/context/AccountContext';
import { getErrorContent } from '../src/utils/error';
import { LoadingCard } from '../src/components/loading/LoadingCard';
import { getPercent } from '../src/utils/helpers';
@ -21,11 +22,10 @@ import { BalanceRoute } from '../src/views/balance/BalanceRoute';
import { Price } from '../src/components/price/Price';
import { useStatusState } from '../src/context/StatusContext';
import { Text } from '../src/components/typography/Styled';
import { useGetChannelsQuery } from '../src/generated/graphql';
const BalanceView = () => {
const { minorVersion } = useStatusState();
const { auth } = useAccount();
const { auth } = useAccountState();
const [outgoing, setOutgoing] = useState<{ id: string } | null>();
const [incoming, setIncoming] = useState();

View File

@ -1,4 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useAccountState } from 'src/context/AccountContext';
import { useGetChannelAmountInfoQuery } from 'src/graphql/queries/__generated__/getNodeInfo.generated';
import { Channels } from '../src/views/channels/channels/Channels';
import { PendingChannels } from '../src/views/channels/pendingChannels/PendingChannels';
import { ClosedChannels } from '../src/views/channels/closedChannels/ClosedChannels';
@ -9,10 +11,8 @@ import {
SingleLine,
ColorButton,
} from '../src/components/generic/Styled';
import { useAccount } from '../src/context/AccountContext';
import { useConfigState } from '../src/context/ConfigContext';
import { textColorMap } from '../src/styles/Themes';
import { useGetChannelAmountInfoQuery } from '../src/generated/graphql';
const ChannelView = () => {
const [view, setView] = useState<number>(1);
@ -23,7 +23,7 @@ const ChannelView = () => {
});
const { theme } = useConfigState();
const { auth } = useAccount();
const { auth } = useAccountState();
const { data } = useGetChannelAmountInfoQuery({
skip: !auth,

View File

@ -1,6 +1,9 @@
import React, { useState } from 'react';
import { toast } from 'react-toastify';
import { ChevronRight, X } from 'react-feather';
import { useAccountState } from 'src/context/AccountContext';
import { useChannelFeesQuery } from 'src/graphql/queries/__generated__/getChannelFees.generated';
import { useUpdateFeesMutation } from 'src/graphql/mutations/__generated__/updateFees.generated';
import {
Card,
CardWithTitle,
@ -13,7 +16,6 @@ import {
ResponsiveLine,
NoWrapTitle,
} from '../src/components/generic/Styled';
import { useAccount } from '../src/context/AccountContext';
import { getErrorContent } from '../src/utils/error';
import { LoadingCard } from '../src/components/loading/LoadingCard';
import { FeeCard } from '../src/views/fees/FeeCard';
@ -21,10 +23,6 @@ import { SecureButton } from '../src/components/buttons/secureButton/SecureButto
import { AdminSwitch } from '../src/components/adminSwitch/AdminSwitch';
import { ColorButton } from '../src/components/buttons/colorButton/ColorButton';
import { Input } from '../src/components/input/Input';
import {
useChannelFeesQuery,
useUpdateFeesMutation,
} from '../src/generated/graphql';
const FeesView = () => {
const [indexOpen, setIndexOpen] = useState(0);
@ -32,7 +30,7 @@ const FeesView = () => {
const [baseFee, setBaseFee] = useState(0);
const [feeRate, setFeeRate] = useState(0);
const { auth } = useAccount();
const { auth } = useAccountState();
const { loading, data } = useChannelFeesQuery({
skip: !auth,

View File

@ -1,5 +1,7 @@
import React, { useState } from 'react';
import { toast } from 'react-toastify';
import { useAccountState } from 'src/context/AccountContext';
import { useGetForwardsQuery } from 'src/graphql/queries/__generated__/getForwards.generated';
import {
SubTitle,
Card,
@ -8,14 +10,12 @@ import {
ColorButton,
SingleLine,
} from '../src/components/generic/Styled';
import { useAccount } from '../src/context/AccountContext';
import { getErrorContent } from '../src/utils/error';
import { LoadingCard } from '../src/components/loading/LoadingCard';
import { ForwardCard } from '../src/views/forwards/ForwardsCard';
import { textColorMap } from '../src/styles/Themes';
import { useConfigState } from '../src/context/ConfigContext';
import { ForwardBox } from '../src/views/home/reports/forwardReport';
import { useGetForwardsQuery } from '../src/generated/graphql';
const timeMap: { [key: string]: string } = {
day: 'today',
@ -29,7 +29,7 @@ const ForwardsView = () => {
const [indexOpen, setIndexOpen] = useState(0);
const { theme } = useConfigState();
const { auth } = useAccount();
const { auth } = useAccountState();
const { loading, data } = useGetForwardsQuery({
skip: !auth,

View File

@ -1,73 +1,19 @@
import React, { useEffect } from 'react';
import { useRouter } from 'next/router';
import { toast } from 'react-toastify';
import { useAccount } from '../src/context/AccountContext';
import * as React from 'react';
import { Spacer } from 'src/components/spacer/Spacer';
import { SessionLogin } from '../src/views/login/SessionLogin';
import { appendBasePath } from '../src/utils/basePath';
import { TopSection } from '../src/views/homepage/Top';
import { LoginBox } from '../src/views/homepage/LoginBox';
import { Accounts } from '../src/views/homepage/Accounts';
import {
useStatusState,
useStatusDispatch,
} from '../src/context/StatusContext';
import { useGetCanConnectLazyQuery } from '../src/generated/graphql';
import { useStatusState } from '../src/context/StatusContext';
import { LoadingCard } from '../src/components/loading/LoadingCard';
import { Section } from '../src/components/section/Section';
const ContextApp = () => {
const { push } = useRouter();
const {
name,
host,
cert,
admin,
viewOnly,
sessionAdmin,
accounts,
} = useAccount();
const { loading: statusLoading } = useStatusState();
const dispatch = useStatusDispatch();
const change = accounts.length <= 1 && admin === '';
const isSession = admin !== '' && viewOnly === '';
const [getCanConnect, { data, loading, error }] = useGetCanConnectLazyQuery({
fetchPolicy: 'network-only',
onError: () => {
toast.error(`Unable to connect to ${name}`);
dispatch({ type: 'disconnected' });
},
});
useEffect(() => {
if (loading && !error) {
dispatch({ type: 'loading' });
}
if (!loading && data?.getNodeInfo && !error) {
dispatch({ type: 'connected' });
push(appendBasePath('/home'));
}
}, [loading, data, error, dispatch, push]);
useEffect(() => {
if (viewOnly !== '' || sessionAdmin !== '') {
getCanConnect({
variables: {
auth: {
host,
macaroon: viewOnly !== '' ? viewOnly : sessionAdmin,
cert,
},
},
});
}
}, [viewOnly, sessionAdmin, getCanConnect, cert, host]);
const { loading } = useStatusState();
return (
<>
<TopSection />
{statusLoading && (
{loading && (
<Section withColor={false}>
<LoadingCard
inverseColor={true}
@ -76,13 +22,14 @@ const ContextApp = () => {
/>
</Section>
)}
{!statusLoading && (
{!loading && (
<>
{isSession && <SessionLogin />}
<Accounts change={!isSession} />
<LoginBox change={change} />
<SessionLogin />
<Accounts />
<LoginBox />
</>
)}
<Spacer />
</>
);
};

View File

@ -1,5 +1,6 @@
import React, { useState } from 'react';
import { useAccount } from '../src/context/AccountContext';
import { useAccountState } from 'src/context/AccountContext';
import { useGetPeersQuery } from 'src/graphql/queries/__generated__/getPeers.generated';
import {
CardWithTitle,
SubTitle,
@ -8,11 +9,10 @@ import {
import { PeersCard } from '../src/views/peers/PeersCard';
import { LoadingCard } from '../src/components/loading/LoadingCard';
import { AddPeer } from '../src/views/peers/AddPeer';
import { useGetPeersQuery } from '../src/generated/graphql';
const PeersView = () => {
const [indexOpen, setIndexOpen] = useState(0);
const { auth } = useAccount();
const { auth } = useAccountState();
const { loading, data } = useGetPeersQuery({
skip: !auth,

View File

@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { toast } from 'react-toastify';
import { useRouter } from 'next/router';
import { useGetOffersQuery } from 'src/graphql/hodlhodl/__generated__/query.generated';
import {
CardWithTitle,
SubTitle,
@ -14,7 +15,6 @@ import { OfferFilters } from '../src/views/trading/OfferFilters';
import { Link } from '../src/components/link/Link';
import { ColorButton } from '../src/components/buttons/colorButton/ColorButton';
import { decode } from '../src/utils/helpers';
import { useGetOffersQuery } from '../src/generated/graphql';
export interface QueryProps {
pagination: {

View File

@ -1,25 +1,28 @@
import React, { useState, useEffect } from 'react';
import { toast } from 'react-toastify';
import { useAccountState } from 'src/context/AccountContext';
import { InvoiceCard } from 'src/views/transactions/InvoiceCard';
import {
useGetResumeQuery,
GetResumeQuery,
} from 'src/graphql/queries/__generated__/getResume.generated';
import {
Card,
CardWithTitle,
SubTitle,
} from '../src/components/generic/Styled';
import { InvoiceCard } from '../src/views/transactions/InvoiceCard';
import { useAccount } from '../src/context/AccountContext';
import { getErrorContent } from '../src/utils/error';
import { PaymentsCard } from '../src/views/transactions/PaymentsCards';
import { LoadingCard } from '../src/components/loading/LoadingCard';
import { ColorButton } from '../src/components/buttons/colorButton/ColorButton';
import { FlowBox } from '../src/views/home/reports/flow';
import { useGetResumeQuery, GetResumeQuery } from '../src/generated/graphql';
const TransactionsView = () => {
const [indexOpen, setIndexOpen] = useState(0);
const [token, setToken] = useState('');
const [fetching, setFetching] = useState(false);
const { auth } = useAccount();
const { auth } = useAccountState();
const { loading, data, fetchMore } = useGetResumeQuery({
skip: !auth,

View File

@ -0,0 +1,49 @@
import * as React from 'react';
import { useRouter } from 'next/router';
import Cookies from 'js-cookie';
import {
useAccountState,
useAccountDispatch,
SSO_ACCOUNT,
} from 'src/context/AccountContext';
import { appendBasePath } from 'src/utils/basePath';
import { useGetAuthTokenQuery } from 'src/graphql/queries/__generated__/getAuthToken.generated';
type AuthCheckProps = {
cookieParam: string | null;
};
export const AuthSSOCheck = ({ cookieParam }: AuthCheckProps) => {
const { push } = useRouter();
const { accounts, ssoSaved } = useAccountState();
const dispatch = useAccountDispatch();
const { data, loading } = useGetAuthTokenQuery({
skip: !cookieParam,
variables: { cookie: cookieParam },
errorPolicy: 'ignore',
});
React.useEffect(() => {
if (cookieParam && !loading && data?.getAuthToken && !ssoSaved) {
Cookies.set('SSOAuth', data.getAuthToken, {
sameSite: 'strict',
});
dispatch({
type: 'addAccounts',
accountsToAdd: [
{
name: 'SSO Account',
id: SSO_ACCOUNT,
loggedIn: true,
type: SSO_ACCOUNT,
},
],
isSSO: true,
});
push(appendBasePath('/'));
}
}, [push, data, loading, cookieParam, accounts, dispatch, ssoSaved]);
return null;
};

View File

@ -0,0 +1,61 @@
import * as React from 'react';
import {
useAccountDispatch,
SERVER_ACCOUNT,
SSO_ACCOUNT,
useAccountState,
} from 'src/context/AccountContext';
import { addIdAndTypeToAccount } from 'src/context/helpers/context';
import { useGetServerAccountsQuery } from 'src/graphql/queries/__generated__/getServerAccounts.generated';
import { toast } from 'react-toastify';
import { useRouter } from 'next/router';
import { appendBasePath } from 'src/utils/basePath';
export const ServerAccounts = () => {
const { hasAccount } = useAccountState();
const dispatch = useAccountDispatch();
const { push, pathname } = useRouter();
const { data, loading } = useGetServerAccountsQuery();
React.useEffect(() => {
if (hasAccount === 'error' && pathname !== '/') {
toast.error('No account found');
dispatch({ type: 'resetFetch' });
push(appendBasePath('/'));
}
}, [hasAccount, push, dispatch, pathname]);
React.useEffect(() => {
const session = sessionStorage.getItem('session') || null;
const changeId = localStorage.getItem('active') || null;
const savedAccounts = JSON.parse(localStorage.getItem('accounts') || '[]');
const accountsToAdd = savedAccounts.map(a => addIdAndTypeToAccount(a));
dispatch({
type: 'initialize',
accountsToAdd,
changeId,
session,
});
}, [dispatch]);
React.useEffect(() => {
if (!loading && data?.getServerAccounts) {
const accountsToAdd = data.getServerAccounts.map(a => {
const type = a?.id === SSO_ACCOUNT ? SSO_ACCOUNT : SERVER_ACCOUNT;
return {
name: a.name,
id: a.id,
loggedIn: a.loggedIn,
type,
};
});
dispatch({
type: 'addAccounts',
accountsToAdd,
});
}
}, [loading, data, dispatch]);
return null;
};

View File

@ -1,14 +1,12 @@
import { useAccount } from '../../context/AccountContext';
import { useAccountState, CLIENT_ACCOUNT } from 'src/context/AccountContext';
interface AdminSwitchProps {
children: any;
}
export const AdminSwitch = ({ children }) => {
const { account, session } = useAccountState();
export const AdminSwitch = ({ children }: AdminSwitchProps) => {
const { admin, sessionAdmin } = useAccount();
if (!admin && !sessionAdmin) {
return null;
if (account?.type === CLIENT_ACCOUNT) {
if (!account.admin && !session) {
return null;
}
}
return children;

View File

@ -1,9 +1,10 @@
import React from 'react';
import ScaleLoader from 'react-spinners/ScaleLoader';
import { X, Check } from 'react-feather';
import { getAuthObj } from 'src/utils/auth';
import { useGetCanAdminQuery } from 'src/graphql/queries/__generated__/adminCheck.generated';
import { SingleLine, Sub4Title } from '../../generic/Styled';
import { themeColors } from '../../../styles/Themes';
import { useGetCanAdminQuery } from '../../../generated/graphql';
type AdminProps = {
host: string;
@ -16,7 +17,7 @@ export const AdminCheck = ({ host, admin, cert, setChecked }: AdminProps) => {
const { data, loading } = useGetCanAdminQuery({
fetchPolicy: 'network-only',
skip: !admin,
variables: { auth: { host, macaroon: admin, cert } },
variables: { auth: getAuthObj(host, null, admin, cert) },
onError: () => {
setChecked(false);
},

View File

@ -1,11 +1,12 @@
import React, { useState, useEffect } from 'react';
import ScaleLoader from 'react-spinners/ScaleLoader';
import { Check, X } from 'react-feather';
import { getAuthObj } from 'src/utils/auth';
import { useGetCanConnectQuery } from 'src/graphql/queries/__generated__/getNodeInfo.generated';
import { themeColors } from '../../../styles/Themes';
import { SingleLine, Sub4Title, Separation } from '../../generic/Styled';
import { ColorButton } from '../../buttons/colorButton/ColorButton';
import { Text } from '../../typography/Styled';
import { useGetCanConnectQuery } from '../../../generated/graphql';
import { AdminCheck } from './AdminCheck';
type ViewProps = {
@ -35,7 +36,9 @@ export const ViewCheck = ({
const { data, loading } = useGetCanConnectQuery({
fetchPolicy: 'network-only',
variables: { auth: { host, macaroon: viewOnly ?? admin ?? '', cert } },
variables: {
auth: getAuthObj(host, viewOnly, admin, cert),
},
onCompleted: () => setConfirmed(true),
onError: () => setConfirmed(false),
});

View File

@ -1,10 +1,13 @@
import React, { useState } from 'react';
import dynamic from 'next/dynamic';
import CryptoJS from 'crypto-js';
import AES from 'crypto-js/aes';
import { useRouter } from 'next/router';
import { toast } from 'react-toastify';
import { useAccount } from '../../context/AccountContext';
import { saveUserAuth, getAccountId } from '../../utils/auth';
import {
useAccountState,
useAccountDispatch,
} from 'src/context/AccountContext';
import { getAccountId } from '../../utils/auth';
import { useStatusDispatch } from '../../context/StatusContext';
import { LoadingCard } from '../loading/LoadingCard';
import { appendBasePath } from '../../utils/basePath';
@ -36,11 +39,12 @@ type AuthProps = {
};
export const Auth = ({ type, status, callback, setStatus }: AuthProps) => {
const { changeAccount, accounts } = useAccount();
const { accounts } = useAccountState();
const { push } = useRouter();
const dispatchChat = useChatDispatch();
const dispatch = useStatusDispatch();
const dispatchChat = useChatDispatch();
const dispatchAccount = useAccountDispatch();
const [name, setName] = useState<string>();
const [host, setHost] = useState<string>();
@ -64,20 +68,12 @@ export const Auth = ({ type, status, callback, setStatus }: AuthProps) => {
viewOnly?: string;
cert?: string;
}) => {
saveUserAuth({
name,
host: host || '',
admin,
viewOnly,
cert,
accounts,
});
const id = getAccountId(host, viewOnly, admin, cert);
dispatch({ type: 'disconnected' });
dispatchChat({ type: 'disconnected' });
changeAccount(id);
dispatchAccount({
type: 'addAccountAndSave',
accountToAdd: { name, host, admin, viewOnly, cert },
});
push(appendBasePath('/'));
};
@ -102,8 +98,9 @@ export const Auth = ({ type, status, callback, setStatus }: AuthProps) => {
cert ?? ''
);
const accountExists =
accounts.filter(account => account.id === id).length > 0;
const accountExists = accounts
? accounts.filter(account => account.id === id).length > 0
: false;
if (accountExists) {
toast.error('Account already exists.');
@ -129,32 +126,29 @@ export const Auth = ({ type, status, callback, setStatus }: AuthProps) => {
} else if (!admin && !viewOnly) {
toast.error('View-Only or Admin macaroon are needed to connect.');
} else {
let correctViewOnly = viewOnly;
let correctViewOnly = viewOnly || null;
if (!viewOnly && admin && !password) {
correctViewOnly = admin;
}
const encryptedAdmin =
admin && password
? CryptoJS.AES.encrypt(admin, password).toString()
: undefined;
saveUserAuth({
name,
host,
admin: encryptedAdmin,
viewOnly: correctViewOnly,
cert,
accounts,
});
const id = getAccountId(host, correctViewOnly, encryptedAdmin, cert);
admin && password ? AES.encrypt(admin, password).toString() : null;
dispatch({ type: 'disconnected' });
dispatchChat({ type: 'disconnected' });
changeAccount(id);
dispatchAccount({
type: 'addAccountAndSave',
accountToAdd: {
name,
host,
admin: encryptedAdmin,
viewOnly: correctViewOnly,
cert,
},
...(!correctViewOnly && { session: admin }),
});
push(appendBasePath('/'));
push(appendBasePath('/home'));
}
};

View File

@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { useGetBitcoinFeesQuery } from 'src/graphql/queries/__generated__/getBitcoinFees.generated';
import { useBitcoinDispatch } from '../../context/BitcoinContext';
import { useGetBitcoinFeesQuery } from '../../generated/graphql';
import { useConfigState } from '../../context/ConfigContext';
export const BitcoinFees = () => {

View File

@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { useGetBitcoinPriceQuery } from 'src/graphql/queries/__generated__/getBitcoinPrice.generated';
import { usePriceDispatch } from '../../context/PriceContext';
import { useGetBitcoinPriceQuery } from '../../generated/graphql';
import { useConfigState } from '../../context/ConfigContext';
export const BitcoinPrice = () => {

View File

@ -2,14 +2,17 @@ import React, { useState } from 'react';
import CryptoJS from 'crypto-js';
import { toast } from 'react-toastify';
import { ChevronRight } from 'react-feather';
import {
useAccountState,
useAccountDispatch,
} from 'src/context/AccountContext';
import { getAuthFromAccount } from 'src/context/helpers/context';
import {
Sub4Title,
NoWrapTitle,
SubTitle,
ResponsiveLine,
} from '../../generic/Styled';
import { useAccount } from '../../../context/AccountContext';
import { saveSessionAuth } from '../../../utils/auth';
import { ColorButton } from '../colorButton/ColorButton';
import { Input } from '../../input/Input';
import { MultiButton, SingleButton } from '../multiButton/MultiButton';
@ -31,7 +34,9 @@ export const LoginModal = ({
}: LoginProps) => {
const [pass, setPass] = useState<string>('');
const [storeSession, setStoreSession] = useState<boolean>(false);
const { host, cert, refreshAccount } = useAccount();
const { account } = useAccountState();
const dispatch = useAccountDispatch();
const handleClick = () => {
try {
@ -39,11 +44,16 @@ export const LoginModal = ({
const decrypted = bytes.toString(CryptoJS.enc.Utf8);
if (storeSession) {
saveSessionAuth(decrypted);
refreshAccount();
dispatch({ type: 'addSession', session: decrypted });
}
const auth = { host, macaroon: decrypted, cert };
callback({ variables: { ...variables, auth } });
callback({
variables: {
...variables,
auth: getAuthFromAccount(account, decrypted),
},
});
setModalOpen(false);
} catch (error) {
toast.error('Wrong Password');

View File

@ -1,13 +1,13 @@
import React, { useState } from 'react';
import { useAccountState, CLIENT_ACCOUNT } from 'src/context/AccountContext';
import { getAuthFromAccount } from 'src/context/helpers/context';
import Modal from '../../modal/ReactModal';
import { useAccount } from '../../../context/AccountContext';
import { ColorButton, ColorButtonProps } from '../colorButton/ColorButton';
import { LoginModal } from './LoginModal';
interface SecureButtonProps extends ColorButtonProps {
callback: any;
disabled: boolean;
children: any;
variables: {};
color?: string;
withMargin?: string;
@ -25,19 +25,20 @@ export const SecureButton: React.FC<SecureButtonProps> = ({
}) => {
const [modalOpen, setModalOpen] = useState<boolean>(false);
const { host, cert, admin, sessionAdmin } = useAccount();
const { session, account } = useAccountState();
if (!admin && !sessionAdmin) {
if (account.type === CLIENT_ACCOUNT && !account.admin && !session) {
return null;
}
const auth = { host, macaroon: sessionAdmin, cert };
const auth = getAuthFromAccount(account, session);
const handleClick = () => setModalOpen(true);
const onClick = sessionAdmin
? () => callback({ variables: { ...variables, auth } })
: handleClick;
const onClick =
session || account.type !== CLIENT_ACCOUNT
? () => callback({ variables: { ...variables, auth } })
: handleClick;
return (
<>
@ -49,15 +50,17 @@ export const SecureButton: React.FC<SecureButtonProps> = ({
>
{children}
</ColorButton>
<Modal isOpen={modalOpen} closeCallback={() => setModalOpen(false)}>
<LoginModal
color={color}
macaroon={admin}
setModalOpen={setModalOpen}
callback={callback}
variables={variables}
/>
</Modal>
{account.type === CLIENT_ACCOUNT && (
<Modal isOpen={modalOpen} closeCallback={() => setModalOpen(false)}>
<LoginModal
color={color}
macaroon={account.admin}
setModalOpen={setModalOpen}
callback={callback}
variables={variables}
/>
</Modal>
)}
</>
);
};

View File

@ -1,11 +1,11 @@
import React, { useState } from 'react';
import { useAccountState, CLIENT_ACCOUNT } from 'src/context/AccountContext';
import { getAuthFromAccount } from 'src/context/helpers/context';
import Modal from '../../modal/ReactModal';
import { useAccount } from '../../../context/AccountContext';
import { LoginModal } from './LoginModal';
interface SecureButtonProps {
callback: any;
children: any;
variables: {};
color?: string;
}
@ -18,34 +18,37 @@ export const SecureWrapper: React.FC<SecureButtonProps> = ({
}) => {
const [modalOpen, setModalOpen] = useState<boolean>(false);
const { host, cert, admin, sessionAdmin } = useAccount();
const { account, session } = useAccountState();
if (!admin && !sessionAdmin) {
if (account.type === CLIENT_ACCOUNT && !account.admin && !session) {
return null;
}
const auth = { host, macaroon: sessionAdmin, cert };
const auth = getAuthFromAccount(account, session);
const handleClick = () => setModalOpen(true);
const onClick = sessionAdmin
? () => callback({ variables: { ...variables, auth } })
: handleClick;
const onClick =
session || account.type !== CLIENT_ACCOUNT
? () => callback({ variables: { ...variables, auth } })
: handleClick;
return (
<>
<div role={'button'} onClick={onClick} onKeyDown={onClick} tabIndex={0}>
{children}
</div>
<Modal isOpen={modalOpen} closeCallback={() => setModalOpen(false)}>
<LoginModal
color={color}
macaroon={admin}
setModalOpen={setModalOpen}
callback={callback}
variables={variables}
/>
</Modal>
{account.type === CLIENT_ACCOUNT && (
<Modal isOpen={modalOpen} closeCallback={() => setModalOpen(false)}>
<LoginModal
color={color}
macaroon={account.admin}
setModalOpen={setModalOpen}
callback={callback}
variables={variables}
/>
</Modal>
)}
</>
);
};

View File

@ -1,9 +1,9 @@
import * as React from 'react';
import { toast } from 'react-toastify';
import { useRouter } from 'next/router';
import { useAccountState } from 'src/context/AccountContext';
import { useGetMessagesQuery } from 'src/graphql/queries/__generated__/getMessages.generated';
import { useChatState, useChatDispatch } from '../../context/ChatContext';
import { useGetMessagesQuery } from '../../generated/graphql';
import { useAccount } from '../../context/AccountContext';
import { getErrorContent } from '../../utils/error';
import { useConfigState } from '../../context/ConfigContext';
@ -12,7 +12,7 @@ export const ChatFetcher = () => {
const { chatPollingSpeed } = useConfigState();
const { auth } = useAccount();
const { auth } = useAccountState();
const { pathname } = useRouter();
const { lastChat, chats, sentChats, initialized } = useChatState();
const dispatch = useChatDispatch();

View File

@ -1,12 +1,12 @@
import * as React from 'react';
import { toast } from 'react-toastify';
import { useAccountState } from 'src/context/AccountContext';
import { useGetMessagesLazyQuery } from 'src/graphql/queries/__generated__/getMessages.generated';
import { useChatDispatch } from '../../context/ChatContext';
import { useGetMessagesLazyQuery } from '../../generated/graphql';
import { useAccount } from '../../context/AccountContext';
import { getErrorContent } from '../../utils/error';
export const ChatInit = () => {
const { auth, id } = useAccount();
const { auth, account } = useAccountState();
const dispatch = useChatDispatch();
const [
@ -18,25 +18,28 @@ export const ChatInit = () => {
});
React.useEffect(() => {
const storageChats = localStorage.getItem(`${id}-sentChats`) || '';
if (account) {
const storageChats =
localStorage.getItem(`${account.id}-sentChats`) || '';
if (storageChats !== '') {
try {
const savedChats = JSON.parse(storageChats);
if (savedChats.length > 0) {
const sender = savedChats[0].sender;
dispatch({
type: 'initialized',
sentChats: savedChats,
sender,
});
if (storageChats !== '') {
try {
const savedChats = JSON.parse(storageChats);
if (savedChats.length > 0) {
const sender = savedChats[0].sender;
dispatch({
type: 'initialized',
sentChats: savedChats,
sender,
});
}
} catch (error) {
localStorage.removeItem('sentChats');
}
} catch (error) {
localStorage.removeItem('sentChats');
}
getMessages();
}
getMessages();
}, [dispatch, getMessages, id]);
}, [dispatch, getMessages, account]);
React.useEffect(() => {
if (!initLoading && !initError && initData?.getMessages) {

View File

@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { AlertTriangle } from 'react-feather';
import styled from 'styled-components';
import { toast } from 'react-toastify';
import { useCloseChannelMutation } from 'src/graphql/mutations/__generated__/closeChannel.generated';
import {
Separation,
SingleLine,
@ -17,7 +18,6 @@ import {
} from '../../buttons/multiButton/MultiButton';
import { Input } from '../../input/Input';
import { useBitcoinState } from '../../../context/BitcoinContext';
import { useCloseChannelMutation } from '../../../generated/graphql';
interface CloseChannelProps {
setModalOpen: (status: boolean) => void;

View File

@ -2,11 +2,11 @@ import React from 'react';
import { AlertTriangle } from 'react-feather';
import styled from 'styled-components';
import { toast } from 'react-toastify';
import { useRemovePeerMutation } from 'src/graphql/mutations/__generated__/removePeer.generated';
import { SubTitle } from '../../generic/Styled';
import { getErrorContent } from '../../../utils/error';
import { SecureButton } from '../../buttons/secureButton/SecureButton';
import { ColorButton } from '../../buttons/colorButton/ColorButton';
import { useRemovePeerMutation } from '../../../generated/graphql';
interface RemovePeerProps {
setModalOpen: (status: boolean) => void;

View File

@ -2,8 +2,8 @@ import * as React from 'react';
import { HelpCircle } from 'react-feather';
import styled from 'styled-components';
import ReactTooltip from 'react-tooltip';
import { useAccountState, CLIENT_ACCOUNT } from 'src/context/AccountContext';
import { CardWithTitle, SubTitle } from '../generic/Styled';
import { useAccount } from '../../context/AccountContext';
import { useConfigState } from '../../context/ConfigContext';
import { getTooltipType } from '../generic/helpers';
import {
@ -19,13 +19,15 @@ const StyledQuestion = styled(HelpCircle)`
`;
export const NodeBar = () => {
const { accounts } = useAccount();
const { accounts } = useAccountState();
const { multiNodeInfo, theme } = useConfigState();
const slider = React.useRef<HTMLDivElement>(null);
const tooltipType: any = getTooltipType(theme);
const viewOnlyAccounts = accounts.filter(account => account.viewOnly !== '');
const viewOnlyAccounts = accounts.filter(
account => account.type === CLIENT_ACCOUNT && account.viewOnly !== ''
);
const handleScroll = (decrease?: boolean) => {
if (slider.current !== null) {

View File

@ -2,11 +2,11 @@ import React, { useState } from 'react';
import { useInView } from 'react-intersection-observer';
import 'intersection-observer'; // Polyfill
import ScaleLoader from 'react-spinners/ScaleLoader';
import { useGetNodeInfoQuery } from 'src/graphql/queries/__generated__/getNodeInfo.generated';
import { SingleLine, DarkSubTitle, ResponsiveLine } from '../generic/Styled';
import { themeColors } from '../../styles/Themes';
import { Price } from '../price/Price';
import Modal from '../modal/ReactModal';
import { useGetNodeInfoQuery } from '../../generated/graphql';
import { getAuthObj } from '../../utils/auth';
import { StatusDot, StatusLine, QuickCard } from './NodeInfo.styled';
import { NodeInfoModal } from './NodeInfoModal';
@ -29,7 +29,7 @@ export const NodeCard = ({ account, accountId }: NodeCardProps) => {
triggerOnce: true,
});
const auth = getAuthObj(host, viewOnly, '', cert);
const auth = getAuthObj(host, viewOnly, null, cert);
const { data, loading, error } = useGetNodeInfoQuery({
skip: !inView || !auth,

View File

@ -1,4 +1,5 @@
import React from 'react';
import { useAccountDispatch } from 'src/context/AccountContext';
import {
SubTitle,
SingleLine,
@ -9,7 +10,6 @@ import {
import { Price } from '../price/Price';
import { ColorButton } from '../buttons/colorButton/ColorButton';
import { useStatusDispatch } from '../../context/StatusContext';
import { useAccount } from '../../context/AccountContext';
interface NodeInfoModalProps {
account: any;
@ -19,7 +19,7 @@ interface NodeInfoModalProps {
export const NodeInfoModal = ({ account, accountId }: NodeInfoModalProps) => {
const dispatch = useStatusDispatch();
const { changeAccount } = useAccount();
const dispatchAccount = useAccountDispatch();
const {
active_channels_count,
@ -89,7 +89,7 @@ export const NodeInfoModal = ({ account, accountId }: NodeInfoModalProps) => {
dispatch({
type: 'disconnected',
});
changeAccount(accountId);
dispatchAccount({ type: 'changeAccount', changeId: accountId });
}}
>
Change to this Account

View File

@ -0,0 +1,13 @@
import * as React from 'react';
import styled from 'styled-components';
import { mediaWidths } from 'src/styles/Themes';
const StyledSpacer = styled.div`
height: 32px;
@media (${mediaWidths.mobile}) {
height: 0;
}
`;
export const Spacer = () => <StyledSpacer />;

View File

@ -1,18 +1,18 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { toast } from 'react-toastify';
import { useAccount } from '../../context/AccountContext';
import { useAccountState } from 'src/context/AccountContext';
import { useGetNodeInfoQuery } from 'src/graphql/queries/__generated__/getNodeInfo.generated';
import { useStatusDispatch } from '../../context/StatusContext';
import { useGetNodeInfoQuery } from '../../generated/graphql';
import { appendBasePath } from '../../utils/basePath';
export const StatusCheck = () => {
const dispatch = useStatusDispatch();
const { push } = useRouter();
const { name, auth } = useAccount();
const { account, auth } = useAccountState();
const { data, loading, error } = useGetNodeInfoQuery({
const { data, loading, error, stopPolling } = useGetNodeInfoQuery({
skip: !auth,
fetchPolicy: 'network-only',
variables: { auth },
@ -21,7 +21,8 @@ export const StatusCheck = () => {
useEffect(() => {
if (error) {
toast.error(`Unable to connect to ${name}`);
toast.error(`Unable to connect to ${account.name}`);
stopPolling();
dispatch({ type: 'disconnected' });
push(appendBasePath('/'));
}
@ -54,7 +55,7 @@ export const StatusCheck = () => {
dispatch({ type: 'connected', state });
}
}, [data, dispatch, error, loading, name, push]);
}, [data, dispatch, error, loading, push, account]);
return null;
};

View File

@ -1,166 +1,308 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import React, { createContext, useState, useContext, useEffect } from 'react';
import merge from 'lodash.merge';
import { getAuth, getAuthObj } from '../utils/auth';
import { saveAccounts } from '../utils/storage';
import * as React from 'react';
import Cookies from 'js-cookie';
import {
getAccountById,
deleteAccountById,
addIdAndTypeToAccount,
getAuthFromAccount,
} from './helpers/context';
interface SingleAccountProps {
export type SERVER_ACCOUNT_TYPE = 'sso' | 'server';
export type ACCOUNT_TYPE = 'client';
export const CLIENT_ACCOUNT: ACCOUNT_TYPE = 'client';
export const SSO_ACCOUNT: SERVER_ACCOUNT_TYPE = 'sso';
export const SERVER_ACCOUNT: SERVER_ACCOUNT_TYPE = 'server';
type HasAccountType = 'fetched' | 'false' | 'error';
export type AccountProps = {
name: string;
host: string;
admin: string;
viewOnly: string;
cert: string;
id: string;
}
};
interface AuthProps {
host: string;
admin: string;
viewOnly: string;
cert: string;
}
interface ChangeProps {
name?: string;
host?: string;
admin?: string;
sessionAdmin?: string;
viewOnly?: string;
cert?: string;
id?: string;
}
interface AccountProps {
name: string;
host: string;
admin: string;
sessionAdmin: string;
viewOnly: string;
cert: string;
id: string;
auth: AuthProps | undefined;
accounts: SingleAccountProps[];
changeAccount: (account: string) => void;
deleteAccount: (account: string) => void;
refreshAccount: () => void;
}
export const AccountContext = createContext<AccountProps>({
name: '',
host: '',
admin: '',
sessionAdmin: '',
viewOnly: '',
cert: '',
id: '',
auth: undefined,
accounts: [],
changeAccount: () => ({}),
deleteAccount: () => ({}),
refreshAccount: () => ({}),
});
const AccountProvider = ({ children }: any) => {
useEffect(() => {
refreshAccount();
}, []);
const changeAccount = (changeToId: string) => {
const currentAccounts = JSON.parse(
localStorage.getItem('accounts') || '[]'
);
const index = currentAccounts.findIndex(
(account: any) => account.id === changeToId
);
if (index < 0) return;
sessionStorage.removeItem('session');
localStorage.setItem('active', `${index}`);
refreshAccount(`${index}`);
};
const deleteAccount = (deleteId: string) => {
const currentAccounts = JSON.parse(
localStorage.getItem('accounts') || '[]'
);
const current = currentAccounts.find(
(account: any) => account.id === deleteId
);
if (!current) return;
const isCurrentAccount = current.id === settings.id;
const changedAccounts = [...currentAccounts].filter(
account => account.id !== deleteId
);
const length = changedAccounts.length;
if (isCurrentAccount) {
sessionStorage.removeItem('session');
localStorage.setItem('active', `${length - 1}`);
} else {
const newIndex = changedAccounts.findIndex(
(account: any) => account.id === settings.id
);
localStorage.setItem('active', `${newIndex}`);
export type AuthType =
| {
type: ACCOUNT_TYPE;
host: string;
macaroon: string;
cert: string | null;
}
| {
type: SERVER_ACCOUNT_TYPE;
id: string;
};
saveAccounts(changedAccounts);
export type CompleteAccount =
| ({
type: 'client';
id: string;
} & AccountProps)
| {
type: 'sso' | 'server';
id: string;
name: string;
loggedIn?: boolean;
};
refreshAccount();
};
type State = {
auth: AuthType | null;
activeAccount: string | null;
session: string | null;
ssoSaved: boolean;
account: CompleteAccount | null;
accounts: CompleteAccount[];
hasAccount: HasAccountType;
};
const refreshAccount = (account?: string) => {
const sessionAdmin = sessionStorage.getItem('session') || '';
const { name, host, admin, viewOnly, cert, id, accounts } = getAuth(
account
);
type ActionType =
| {
type: 'initialize';
changeId: string;
accountsToAdd: CompleteAccount[];
session: string;
}
| {
type: 'changeAccount' | 'deleteAccount';
changeId: string;
}
| {
type: 'logout';
}
| {
type: 'addAccounts';
accountsToAdd: CompleteAccount[];
isSSO?: boolean;
}
| {
type: 'addAccountAndSave';
accountToAdd: AccountProps;
session?: string;
}
| {
type: 'addSession';
session: string;
}
| {
type: 'removeSession';
}
| {
type: 'deleteAll';
}
| {
type: 'resetFetch';
};
updateAccount((prevState: any) => {
const newState = { ...prevState };
type Dispatch = (action: ActionType) => void;
const merged = merge(newState, {
name,
host,
admin,
sessionAdmin,
viewOnly,
cert,
id,
auth: getAuthObj(host, viewOnly, sessionAdmin, cert),
});
const StateContext = React.createContext<State | undefined>(undefined);
const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
return { ...merged, accounts };
});
};
const initialState: State = {
auth: null,
session: null,
activeAccount: null,
ssoSaved: false,
account: null,
accounts: [],
hasAccount: 'false',
};
const accountState = {
name: '',
host: '',
admin: '',
sessionAdmin: '',
viewOnly: '',
cert: '',
id: '',
auth: undefined,
accounts: [],
changeAccount,
deleteAccount,
refreshAccount,
};
const stateReducer = (state: State, action: ActionType): State => {
switch (action.type) {
case 'initialize': {
const { accountsToAdd, changeId, session } = action;
const [settings, updateAccount] = useState(accountState);
const { account, id } = getAccountById(changeId, accountsToAdd);
if (!account)
return {
...state,
accounts: accountsToAdd,
activeAccount: changeId,
session,
};
const auth = getAuthFromAccount(account, session);
return {
...state,
auth,
account,
accounts: accountsToAdd,
activeAccount: id,
session,
hasAccount: 'fetched',
};
}
case 'changeAccount': {
const { account, id } = getAccountById(action.changeId, state.accounts);
if (!account) return state;
const auth = getAuthFromAccount(account);
localStorage.setItem('active', `${id}`);
sessionStorage.removeItem('session');
return {
...state,
auth,
session: null,
account,
activeAccount: id,
hasAccount: 'fetched',
};
}
case 'logout':
localStorage.removeItem('active');
sessionStorage.clear();
Cookies.remove('AccountAuth');
Cookies.remove('SSOAuth');
return {
...state,
account: null,
activeAccount: null,
auth: null,
session: null,
};
case 'deleteAccount': {
if (!state.accounts || state?.accounts?.length <= 0) {
return state;
}
const { accounts, id } = deleteAccountById(
state.account.id,
action.changeId,
state.accounts
);
localStorage.setItem('accounts', JSON.stringify(accounts));
!id && sessionStorage.removeItem('session');
return {
...state,
accounts,
...(!id && { activeId: null, session: null, account: null }),
};
}
case 'addAccounts': {
const completeAccounts = [...state.accounts, ...action.accountsToAdd];
if (!state.activeAccount) {
return {
...state,
accounts: completeAccounts,
...(action.isSSO && { ssoSaved: true }),
};
}
const { account } = getAccountById(state.activeAccount, completeAccounts);
if (!account && completeAccounts.length > 0) {
return {
...state,
accounts: completeAccounts,
hasAccount: 'error',
...(action.isSSO && { ssoSaved: true }),
};
}
const auth = getAuthFromAccount(account, state.session);
return {
...state,
hasAccount: 'fetched',
auth,
account,
accounts: completeAccounts,
...(action.isSSO && { ssoSaved: true }),
};
}
case 'addAccountAndSave': {
const account = addIdAndTypeToAccount(action.accountToAdd);
const activeAccount = account.id;
const accounts = [...state.accounts, account];
const auth = getAuthFromAccount(account, action.session);
if (action.session) {
sessionStorage.setItem('session', action.session);
}
localStorage.setItem('active', `${activeAccount}`);
const savedAccounts = JSON.parse(
localStorage.getItem('accounts') || '[]'
);
localStorage.setItem(
'accounts',
JSON.stringify([...savedAccounts, action.accountToAdd])
);
return {
...state,
auth,
account,
accounts,
activeAccount,
hasAccount: 'fetched',
...(action.session && { session: action.session }),
};
}
case 'addSession':
sessionStorage.setItem('session', action.session);
return {
...state,
auth: getAuthFromAccount(state.account, action.session),
session: action.session,
};
case 'removeSession':
sessionStorage.removeItem('session');
return {
...state,
auth: getAuthFromAccount(state.account),
session: null,
};
case 'deleteAll':
localStorage.clear();
sessionStorage.clear();
Cookies.remove('config');
Cookies.remove('AccountAuth');
Cookies.remove('SSOAuth');
return initialState;
case 'resetFetch':
return {
...state,
hasAccount: 'false',
};
default:
return state;
}
};
const AccountProvider: React.FC = ({ children }) => {
const [state, dispatch] = React.useReducer(stateReducer, initialState);
return (
<AccountContext.Provider value={settings}>
{children}
</AccountContext.Provider>
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>{children}</StateContext.Provider>
</DispatchContext.Provider>
);
};
const useAccount = () => useContext(AccountContext);
const useAccountState = () => {
const context = React.useContext(StateContext);
if (context === undefined) {
throw new Error('useAccountState must be used within a AccountProvider');
}
return context;
};
export { AccountProvider, useAccount };
const useAccountDispatch = () => {
const context = React.useContext(DispatchContext);
if (context === undefined) {
throw new Error('useAccountDispatch must be used within a AccountProvider');
}
return context;
};
export { AccountProvider, useAccountState, useAccountDispatch };

View File

@ -13,10 +13,14 @@ type ChangeState = {
hour: number;
};
type ActionType = {
type: 'fetched' | 'dontShow';
state?: ChangeState;
};
type ActionType =
| {
type: 'fetched';
state: ChangeState;
}
| {
type: 'dontShow';
};
type Dispatch = (action: ActionType) => void;
@ -41,7 +45,7 @@ const stateReducer = (state: State, action: ActionType): State => {
}
};
const BitcoinInfoProvider = ({ children }: any) => {
const BitcoinInfoProvider = ({ children }) => {
const [state, dispatch] = useReducer(stateReducer, initialState);
return (

View File

@ -30,20 +30,33 @@ type State = {
sender: string;
};
type ActionType = {
type:
| 'initialized'
| 'additional'
| 'changeActive'
| 'newChat'
| 'disconnected';
chats?: ChatProps[];
sentChats?: SentChatProps[];
newChat?: SentChatProps;
lastChat?: string;
sender?: string;
userId?: string;
};
type ActionType =
| {
type: 'initialized';
chats?: ChatProps[];
lastChat?: string;
sender?: string;
sentChats?: SentChatProps[];
}
| {
type: 'additional';
chats: ChatProps[];
lastChat: string;
}
| {
type: 'changeActive';
sender: string;
userId: string;
}
| {
type: 'newChat';
sender: string;
userId: string;
newChat: SentChatProps;
}
| {
type: 'disconnected';
};
type Dispatch = (action: ActionType) => void;
@ -88,6 +101,8 @@ const stateReducer = (state: State, action: ActionType): State => {
sentChats: [...state.sentChats, action.newChat],
...(action.sender && { sender: action.sender }),
};
case 'disconnected':
return initialState;
default:
return state;
}

View File

@ -14,10 +14,14 @@ type ChangeState = {
prices?: { [key: string]: PriceProps };
};
type ActionType = {
type: 'fetched' | 'dontShow';
state?: ChangeState;
};
type ActionType =
| {
type: 'fetched';
state: ChangeState;
}
| {
type: 'dontShow';
};
type Dispatch = (action: ActionType) => void;
@ -40,7 +44,7 @@ const stateReducer = (state: State, action: ActionType): State => {
}
};
const PriceProvider = ({ children }: any) => {
const PriceProvider = ({ children }) => {
const [state, dispatch] = useReducer(stateReducer, initialState);
return (

View File

@ -60,7 +60,7 @@ const stateReducer = (state: State, action: ActionType): CompleteState => {
}
};
const StatusProvider = ({ children }: any) => {
const StatusProvider = ({ children }) => {
const [state, dispatch] = useReducer(stateReducer, initialState);
return (

View File

@ -0,0 +1,75 @@
import {
CLIENT_ACCOUNT,
SSO_ACCOUNT,
CompleteAccount,
} from '../AccountContext';
import { getAccountById, deleteAccountById } from './context';
const firstAccount = {
name: 'Hola',
host: 'Host1',
admin: 'Admin1',
viewOnly: 'ViewOnly1',
cert: 'Cert1',
id: '123',
type: CLIENT_ACCOUNT,
};
const secondAccount = {
name: 'Chao',
host: 'Host2',
admin: 'Admin2',
viewOnly: 'ViewOnly2',
cert: 'Cert2',
id: '1234',
type: SSO_ACCOUNT,
};
const testAccounts: CompleteAccount[] = [firstAccount, secondAccount];
describe('Context Helpers', () => {
describe('should getAccountById', () => {
test('account exists', () => {
const { account, id } = getAccountById('1234', testAccounts);
expect(id).toBe('1234');
expect(account).toBe(secondAccount);
});
test('account does not exists', () => {
const { account, id } = getAccountById('false id', testAccounts);
expect(id).toBe(null);
expect(account).toBe(null);
});
});
describe('should deleteAccountById', () => {
test('account exists', () => {
const { accounts, id } = deleteAccountById('123', '1234', testAccounts);
expect(id).toBe('123');
expect(accounts).toStrictEqual([firstAccount]);
});
test('account exists and is current account', () => {
const { accounts, id } = deleteAccountById('123', '123', testAccounts);
expect(id).toBe(null);
expect(accounts).toStrictEqual([secondAccount]);
});
test('account does not exists', () => {
const { accounts, id } = deleteAccountById(
'123',
'false id',
testAccounts
);
expect(id).toBe('123');
expect(accounts).toStrictEqual(testAccounts);
});
test('one account', () => {
const { accounts, id } = deleteAccountById('123', '123', [firstAccount]);
expect(id).toBe(null);
expect(accounts).toStrictEqual([]);
});
});
});

View File

@ -0,0 +1,74 @@
import { getUUID } from '../../utils/auth';
import {
CompleteAccount,
AccountProps,
CLIENT_ACCOUNT,
AuthType,
} from '../AccountContext';
export const getAccountById = (id: string, accounts: CompleteAccount[]) => {
const correctAccount: CompleteAccount | null = accounts.find(
a => a.id === id
);
return {
account: correctAccount || null,
id: correctAccount ? correctAccount.id : null,
};
};
export const deleteAccountById = (
currentId: string,
id: string,
accounts: CompleteAccount[]
) => {
const newAccounts: CompleteAccount[] = accounts.filter(a => a.id !== id);
if (newAccounts.length <= 0) {
return { accounts: [], id: null };
}
let activeId: string | null = currentId;
if (currentId === id) {
activeId = null;
}
return { accounts: newAccounts, id: activeId };
};
export const addIdAndTypeToAccount = (
account: AccountProps
): CompleteAccount => {
const { host, viewOnly, admin, cert } = account;
return {
...account,
type: CLIENT_ACCOUNT,
id: getUUID(`${host}-${viewOnly}-${admin !== '' ? 1 : 0}-${cert}`),
};
};
export const getAuthFromAccount = (
account: CompleteAccount,
session?: string
): AuthType => {
if (!account) return null;
if (account.type !== CLIENT_ACCOUNT) {
return {
type: account.type,
id: account.id,
};
}
const { host, viewOnly, cert } = account;
if (!host) {
return null;
}
if (!viewOnly && !session) {
return null;
}
return {
type: account.type,
host,
macaroon: viewOnly && viewOnly !== '' ? viewOnly : session,
cert,
};
};

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More