chore: amboss login

This commit is contained in:
apotdevin 2021-09-12 19:55:05 +02:00
parent 3c051fc811
commit bd6aa992b8
No known key found for this signature in database
GPG Key ID: 4403F1DFBE779457
22 changed files with 515 additions and 6 deletions

View File

@ -0,0 +1,169 @@
import { requestLimiter } from 'server/helpers/rateLimiter';
import { ContextType } from 'server/types/apiTypes';
import { appUrls } from 'server/utils/appUrls';
import { graphqlFetchWithProxy } from 'server/utils/fetch';
import { signMessage } from 'ln-service';
import { toWithError } from 'server/helpers/async';
import cookieLib from 'cookie';
import { appConstants } from 'server/utils/appConstants';
import { logger } from 'server/helpers/logger';
const ONE_MONTH_SECONDS = 60 * 60 * 24 * 30;
const getUserQuery = `
query GetUser {
getUser {
subscription {
end_date
subscribed
upgradable
}
}
}
`;
const getLoginTokenQuery = `
query GetLoginToken(
$seconds: Float
) {
getLoginToken(seconds: $seconds)
}
`;
const getSignInfoQuery = `
query GetSignInfo {
getSignInfo {
expiry
identifier
message
}
}
`;
const loginMutation = `
mutation Login(
$identifier: String!
$signature: String!
$seconds: Float
$details: String
$token: Boolean
) {
login(
identifier: $identifier
signature: $signature
seconds: $seconds
details: $details
token: $token
)
}
`;
export const ambossResolvers = {
Query: {
getAmbossUser: async (
_: undefined,
__: undefined,
{ ip, ambossAuth }: ContextType
) => {
await requestLimiter(ip, 'getAmbossUser');
const { data, error } = await graphqlFetchWithProxy(
appUrls.amboss,
getUserQuery,
undefined,
{
authorization: ambossAuth ? `Bearer ${ambossAuth}` : '',
}
);
if (!data?.getUser || error) {
return null;
}
return data.getUser;
},
getAmbossLoginToken: async (
_: undefined,
__: undefined,
{ ip, ambossAuth }: ContextType
) => {
await requestLimiter(ip, 'getAmbossLoginToken');
const { data, error } = await graphqlFetchWithProxy(
appUrls.amboss,
getLoginTokenQuery,
{ seconds: ONE_MONTH_SECONDS },
{
authorization: ambossAuth ? `Bearer ${ambossAuth}` : '',
}
);
if (!data?.getLoginToken || error) {
throw new Error('Error getting login token from Amboss');
}
return data.getLoginToken;
},
},
Mutation: {
loginAmboss: async (
_: undefined,
__: undefined,
{ ip, lnd, res }: ContextType
) => {
await requestLimiter(ip, 'loginAmboss');
const { data, error } = await graphqlFetchWithProxy(
appUrls.amboss,
getSignInfoQuery
);
if (!data?.getSignInfo || error) {
throw new Error('Error getting login information from Amboss');
}
const [message, signError] = await toWithError<{ signature: string }>(
signMessage({
lnd,
message: data.getSignInfo.message,
})
);
if (!message?.signature || signError) {
throw new Error('Error signing message to login');
}
logger.debug('Signed Amboss login message');
const { identifier } = data.getSignInfo;
const params = {
details: 'ThunderHub',
identifier,
signature: message.signature,
token: true,
seconds: ONE_MONTH_SECONDS,
};
const { data: loginData, error: loginError } =
await graphqlFetchWithProxy(appUrls.amboss, loginMutation, params);
if (!loginData.login || loginError) {
throw new Error('Error logging into Amboss');
}
logger.debug('Got Amboss login token');
res.setHeader(
'Set-Cookie',
cookieLib.serialize(appConstants.ambossCookieName, loginData.login, {
maxAge: ONE_MONTH_SECONDS,
httpOnly: true,
sameSite: true,
path: '/',
})
);
return true;
},
},
};

View File

@ -0,0 +1,13 @@
import { gql } from 'apollo-server-micro';
export const ambossTypes = gql`
type AmbossSubscriptionType {
end_date: String!
subscribed: Boolean!
upgradable: Boolean!
}
type AmbossUserType {
subscription: AmbossSubscriptionType
}
`;

View File

@ -44,6 +44,7 @@ export const getContext = (context: ResolverContext) => {
const auth = cookies[appConstants.cookieName];
const lnMarketsAuth = cookies[appConstants.lnMarketsAuth];
const tokenAuth = cookies[appConstants.tokenCookieName];
const ambossAuth = cookies[appConstants.ambossCookieName];
let lnd: LndObject | null = null;
let id: string | null = null;
@ -70,6 +71,7 @@ export const getContext = (context: ResolverContext) => {
res,
lnMarketsAuth,
tokenAuth,
ambossAuth,
};
return resolverContext;

View File

@ -46,6 +46,8 @@ import { boltzResolvers } from './boltz/resolvers';
import { boltzTypes } from './boltz/types';
import { forwardsResolver } from './forwards/resolvers';
import { macaroonTypes } from './macaroon/types';
import { ambossTypes } from './amboss/types';
import { ambossResolvers } from './amboss/resolvers';
const typeDefs = [
generalTypes,
@ -71,6 +73,7 @@ const typeDefs = [
lnMarketsTypes,
boltzTypes,
macaroonTypes,
ambossTypes,
];
const resolvers = merge(
@ -98,7 +101,8 @@ const resolvers = merge(
lnUrlResolvers,
lnMarketsResolvers,
boltzResolvers,
forwardsResolver
forwardsResolver,
ambossResolvers
);
export const schema = makeExecutableSchema({ typeDefs, resolvers });

View File

@ -28,6 +28,8 @@ export const generalTypes = gql`
export const queryTypes = gql`
type Query {
getAmbossLoginToken: String!
getAmbossUser: AmbossUserType
getNodeBalances: BalancesType!
getBosNodeScores(publicKey: String!): [BosScore]!
getBosScores: BosScoreResponse!
@ -94,6 +96,7 @@ export const queryTypes = gql`
export const mutationTypes = gql`
type Mutation {
loginAmboss: Boolean
getAuthToken(cookie: String): Boolean!
getSessionToken(id: String, password: String): String!
claimBoltzTransaction(

View File

@ -25,6 +25,7 @@ export const ContextMock: ContextType = {
res: {} as ServerResponse,
lnMarketsAuth: 'lnMarketAuth',
tokenAuth: 'tokenAuth',
ambossAuth: 'ambossAuth',
};
export const ContextMockNoAccounts: ContextType = {
@ -41,6 +42,7 @@ export const ContextMockNoAccounts: ContextType = {
res: {} as ServerResponse,
lnMarketsAuth: 'lnMarketAuth',
tokenAuth: 'tokenAuth',
ambossAuth: 'ambossAuth',
};
export const ContextMockNoSSO: ContextType = {
@ -63,4 +65,5 @@ export const ContextMockNoSSO: ContextType = {
res: {} as ServerResponse,
lnMarketsAuth: 'lnMarketAuth',
tokenAuth: 'tokenAuth',
ambossAuth: 'ambossAuth',
};

View File

@ -18,4 +18,5 @@ export type ContextType = {
res: ServerResponse;
lnMarketsAuth: string | null;
tokenAuth: string | null;
ambossAuth: string | null;
};

View File

@ -2,4 +2,5 @@ export const appConstants = {
cookieName: 'Thub-Auth',
lnMarketsAuth: 'LnMarkets-Auth',
tokenCookieName: 'Tbase-Auth',
ambossCookieName: 'Amboss-Auth',
};

View File

@ -15,4 +15,5 @@ export const appUrls = {
lnMarkets: 'https://api.lnmarkets.com/v1',
lnMarketsExchange: 'https://lnmarkets.com',
boltz: 'https://boltz.exchange/api',
amboss: 'https://api.amboss.space/graphql',
};

View File

@ -22,18 +22,30 @@ export const fetchWithProxy = (url: string, options?: {}) => {
export const graphqlFetchWithProxy = async (
url: string,
query: string,
variables?: { [key: string]: string | number | string[] }
variables?: { [key: string]: string | number | string[] | boolean },
headers?: { [key: string]: string | number | string[] | boolean }
): Promise<{
data: any;
error: undefined | GraphQLError;
}> => {
return fetchWithProxy(url, {
method: 'post',
headers: { Accept: 'application/json' },
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(headers || {}),
},
body: JSON.stringify({ query, variables }),
})
.then(res => res.json() as any)
.then(data => data)
.then(result => {
logger.silly(result);
const { data, errors } = result;
return {
data,
error: errors?.[0]?.message,
};
})
.catch(error => {
logger.error('Error doing graphql fetch: %o', error);
return { data: undefined, error };

View File

@ -0,0 +1,42 @@
/* eslint-disable */
import * as Types from '../../types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {}
export type LoginAmbossMutationVariables = Types.Exact<{ [key: string]: never; }>;
export type LoginAmbossMutation = { __typename?: 'Mutation', loginAmboss?: Types.Maybe<boolean> };
export const LoginAmbossDocument = gql`
mutation LoginAmboss {
loginAmboss
}
`;
export type LoginAmbossMutationFn = Apollo.MutationFunction<LoginAmbossMutation, LoginAmbossMutationVariables>;
/**
* __useLoginAmbossMutation__
*
* To run a mutation, you first call `useLoginAmbossMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useLoginAmbossMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [loginAmbossMutation, { data, loading, error }] = useLoginAmbossMutation({
* variables: {
* },
* });
*/
export function useLoginAmbossMutation(baseOptions?: Apollo.MutationHookOptions<LoginAmbossMutation, LoginAmbossMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<LoginAmbossMutation, LoginAmbossMutationVariables>(LoginAmbossDocument, options);
}
export type LoginAmbossMutationHookResult = ReturnType<typeof useLoginAmbossMutation>;
export type LoginAmbossMutationResult = Apollo.MutationResult<LoginAmbossMutation>;
export type LoginAmbossMutationOptions = Apollo.BaseMutationOptions<LoginAmbossMutation, LoginAmbossMutationVariables>;

View File

@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const LOGIN_AMBOSS = gql`
mutation LoginAmboss {
loginAmboss
}
`;

View File

@ -0,0 +1,44 @@
/* eslint-disable */
import * as Types from '../../types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {}
export type GetAmbossLoginTokenQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type GetAmbossLoginTokenQuery = { __typename?: 'Query', getAmbossLoginToken: string };
export const GetAmbossLoginTokenDocument = gql`
query GetAmbossLoginToken {
getAmbossLoginToken
}
`;
/**
* __useGetAmbossLoginTokenQuery__
*
* To run a query within a React component, call `useGetAmbossLoginTokenQuery` and pass it any options that fit your needs.
* When your component renders, `useGetAmbossLoginTokenQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetAmbossLoginTokenQuery({
* variables: {
* },
* });
*/
export function useGetAmbossLoginTokenQuery(baseOptions?: Apollo.QueryHookOptions<GetAmbossLoginTokenQuery, GetAmbossLoginTokenQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetAmbossLoginTokenQuery, GetAmbossLoginTokenQueryVariables>(GetAmbossLoginTokenDocument, options);
}
export function useGetAmbossLoginTokenLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetAmbossLoginTokenQuery, GetAmbossLoginTokenQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetAmbossLoginTokenQuery, GetAmbossLoginTokenQueryVariables>(GetAmbossLoginTokenDocument, options);
}
export type GetAmbossLoginTokenQueryHookResult = ReturnType<typeof useGetAmbossLoginTokenQuery>;
export type GetAmbossLoginTokenLazyQueryHookResult = ReturnType<typeof useGetAmbossLoginTokenLazyQuery>;
export type GetAmbossLoginTokenQueryResult = Apollo.QueryResult<GetAmbossLoginTokenQuery, GetAmbossLoginTokenQueryVariables>;

View File

@ -0,0 +1,50 @@
/* eslint-disable */
import * as Types from '../../types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {}
export type GetAmbossUserQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type GetAmbossUserQuery = { __typename?: 'Query', getAmbossUser?: Types.Maybe<{ __typename?: 'AmbossUserType', subscription?: Types.Maybe<{ __typename?: 'AmbossSubscriptionType', end_date: string, subscribed: boolean, upgradable: boolean }> }> };
export const GetAmbossUserDocument = gql`
query GetAmbossUser {
getAmbossUser {
subscription {
end_date
subscribed
upgradable
}
}
}
`;
/**
* __useGetAmbossUserQuery__
*
* To run a query within a React component, call `useGetAmbossUserQuery` and pass it any options that fit your needs.
* When your component renders, `useGetAmbossUserQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetAmbossUserQuery({
* variables: {
* },
* });
*/
export function useGetAmbossUserQuery(baseOptions?: Apollo.QueryHookOptions<GetAmbossUserQuery, GetAmbossUserQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetAmbossUserQuery, GetAmbossUserQueryVariables>(GetAmbossUserDocument, options);
}
export function useGetAmbossUserLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetAmbossUserQuery, GetAmbossUserQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetAmbossUserQuery, GetAmbossUserQueryVariables>(GetAmbossUserDocument, options);
}
export type GetAmbossUserQueryHookResult = ReturnType<typeof useGetAmbossUserQuery>;
export type GetAmbossUserLazyQueryHookResult = ReturnType<typeof useGetAmbossUserLazyQuery>;
export type GetAmbossUserQueryResult = Apollo.QueryResult<GetAmbossUserQuery, GetAmbossUserQueryVariables>;

View File

@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const GET_AMBOSS_LOGIN_TOKEN = gql`
query GetAmbossLoginToken {
getAmbossLoginToken
}
`;

View File

@ -0,0 +1,13 @@
import { gql } from '@apollo/client';
export const GET_AMBOSS_USER = gql`
query GetAmbossUser {
getAmbossUser {
subscription {
end_date
subscribed
upgradable
}
}
}
`;

View File

@ -18,6 +18,18 @@ export type Scalars = {
Time: any;
};
export type AmbossSubscriptionType = {
__typename?: 'AmbossSubscriptionType';
end_date: Scalars['String'];
subscribed: Scalars['Boolean'];
upgradable: Scalars['Boolean'];
};
export type AmbossUserType = {
__typename?: 'AmbossUserType';
subscription?: Maybe<AmbossSubscriptionType>;
};
export type AuthResponse = {
__typename?: 'AuthResponse';
message: Scalars['String'];
@ -229,6 +241,7 @@ export type Mutation = {
lnUrlChannel: Scalars['String'];
lnUrlPay: PaySuccess;
lnUrlWithdraw: Scalars['String'];
loginAmboss?: Maybe<Scalars['Boolean']>;
logout: Scalars['Boolean'];
openChannel?: Maybe<OpenChannelType>;
pay?: Maybe<Scalars['Boolean']>;
@ -521,6 +534,8 @@ export type Query = {
decodeRequest?: Maybe<DecodeType>;
getAccount?: Maybe<ServerAccountType>;
getAccountingReport: Scalars['String'];
getAmbossLoginToken: Scalars['String'];
getAmbossUser?: Maybe<AmbossUserType>;
getBackups?: Maybe<Scalars['String']>;
getBaseCanConnect: Scalars['Boolean'];
getBaseInfo: BaseInfo;

View File

@ -0,0 +1,11 @@
import { useGetAmbossUserQuery } from 'src/graphql/queries/__generated__/getAmbossUser.generated';
export const useAmbossUser = () => {
const { data, loading } = useGetAmbossUserQuery();
if (loading || !data?.getAmbossUser) {
return null;
}
return data.getAmbossUser.subscription;
};

View File

@ -20,6 +20,7 @@ import { SupportBar } from './donate/DonateContent';
import { OpenChannel } from './openChannel';
import { LnUrlCard } from './lnurl';
import { LnMarketsCard } from './lnmarkets';
import { AmbossCard } from './amboss/AmbossCard';
export const QuickCard = styled.div`
background: ${cardColor};
@ -50,7 +51,7 @@ export const QuickCard = styled.div`
`;
export const QuickTitle = styled.div`
font-size: 14px;
font-size: 12px;
color: ${unSelectedNavButton};
margin-top: 10px;
text-align: center;
@ -99,6 +100,7 @@ export const QuickActions = () => {
return (
<QuickRow>
<SupportCard callback={() => setOpenCard('support')} />
<AmbossCard />
<QuickCard onClick={() => setOpenCard('open_channel')}>
<GitBranch size={24} />
<QuickTitle>Open</QuickTitle>

View File

@ -0,0 +1,109 @@
import { useEffect } from 'react';
import { toast } from 'react-toastify';
import { useLoginAmbossMutation } from 'src/graphql/mutations/__generated__/loginAmboss.generated';
import { useGetAmbossLoginTokenLazyQuery } from 'src/graphql/queries/__generated__/getAmbossLoginToken.generated';
import { useAmbossUser } from 'src/hooks/UseAmbossUser';
import {
cardBorderColor,
cardColor,
mediaWidths,
unSelectedNavButton,
} from 'src/styles/Themes';
import styled from 'styled-components';
import Image from 'next/image';
import ambossLogo from './AmbossLogo.png';
const QuickTitle = styled.div`
font-size: 12px;
color: ${unSelectedNavButton};
margin-top: 10px;
`;
const QuickCard = styled.button`
background: ${cardColor};
box-shadow: 0 8px 16px -8px rgba(0, 0, 0, 0.1);
border-radius: 4px;
border: 1px solid ${cardBorderColor};
height: 100px;
width: 100px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-bottom: 25px;
padding: 10px;
margin-right: 10px;
cursor: pointer;
color: #69c0ff;
@media (${mediaWidths.mobile}) {
padding: 4px;
height: 80px;
width: 80px;
}
&:hover {
background-color: #ff0080;
color: white;
img {
filter: brightness(0) invert(1);
}
& ${QuickTitle} {
color: white;
}
}
`;
export const AmbossCard = () => {
const subscription = useAmbossUser();
const [login, { loading }] = useLoginAmbossMutation({
onCompleted: () => toast.success('Logged in'),
onError: () => toast.error('Error logging in'),
refetchQueries: ['GetAmbossUser'],
});
const [getToken, { data, loading: tokenLoading }] =
useGetAmbossLoginTokenLazyQuery({
onError: () => toast.error('Error getting auth token'),
});
useEffect(() => {
if (!data?.getAmbossLoginToken || tokenLoading) {
return;
}
if (!window?.open) return;
const url = `https://amboss.space/token?key=${data.getAmbossLoginToken}`;
(window as any).open(url, '_blank').focus();
}, [data, tokenLoading]);
if (!subscription) {
return (
<QuickCard
onClick={() => {
if (loading) return;
login();
}}
disabled={loading}
>
<Image src={ambossLogo} width={32} height={32} alt={'Amboss Logo'} />
<QuickTitle>{loading ? 'Loading...' : 'Login'}</QuickTitle>
</QuickCard>
);
}
return (
<QuickCard
onClick={() => {
if (tokenLoading) return;
getToken();
}}
disabled={tokenLoading}
>
<Image src={ambossLogo} width={32} height={32} alt={'Amboss Logo'} />
<QuickTitle>{tokenLoading ? 'Loading...' : 'Go To'}</QuickTitle>
</QuickCard>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -10,7 +10,7 @@ import {
} from 'src/styles/Themes';
const QuickTitle = styled.div`
font-size: 14px;
font-size: 12px;
color: ${unSelectedNavButton};
margin-top: 10px;
`;