|
1 | 1 | import {ApolloLink, createOperation, FetchResult, NextLink, Observable, Operation} from 'apollo-link'; |
| 2 | +import {removeDirectivesFromDocument} from 'apollo-utilities'; |
| 3 | +import { DirectiveNode, FieldNode, OperationDefinitionNode } from 'graphql'; |
2 | 4 | import gql from 'graphql-tag'; |
3 | 5 | import MemoryStorage from './storage/MemoryStorage'; |
4 | 6 | import {IAuthTokenSet, ISlicknodeLinkOptions, IStorage} from './types'; |
@@ -29,6 +31,11 @@ export const LOGOUT_MUTATION = gql`mutation logout($refreshToken: String) { |
29 | 31 | } |
30 | 32 | }`; |
31 | 33 |
|
| 34 | +const authenticationDirectiveRemoveConfig = { |
| 35 | + test: (directive: DirectiveNode) => directive.name.value === 'authenticate', |
| 36 | + remove: false, |
| 37 | +}; |
| 38 | + |
32 | 39 | /** |
33 | 40 | * SlicknodeLink instance to be used to load data with apollo-client |
34 | 41 | * from slicknode GraphQL servers |
@@ -74,15 +81,86 @@ export default class SlicknodeLink extends ApolloLink { |
74 | 81 | ...authHeaders, |
75 | 82 | }, |
76 | 83 | })); |
77 | | - forward(operation).subscribe(observer); |
| 84 | + |
| 85 | + const definitions = operation.query.definitions; |
| 86 | + // Find current operation in definitions |
| 87 | + const currentOperation: OperationDefinitionNode | null = definitions.find((operationDefinition) => { |
| 88 | + return ( |
| 89 | + operationDefinition.kind === 'OperationDefinition' && |
| 90 | + operationDefinition.name && |
| 91 | + operationDefinition.name.value === operation.operationName |
| 92 | + ); |
| 93 | + }) as OperationDefinitionNode | null; |
| 94 | + |
| 95 | + // Check mutations for directives and logoutMutation |
| 96 | + const resultListeners: Array<(value: any) => void> = []; |
| 97 | + if (currentOperation.operation === 'mutation') { |
| 98 | + const fields: FieldNode[] = []; |
| 99 | + currentOperation.selectionSet.selections.forEach((selectionNode) => { |
| 100 | + if (selectionNode.kind === 'Field') { |
| 101 | + fields.push(selectionNode); |
| 102 | + } else { |
| 103 | + // @TODO: Collect all relevant fields recursively from fragments / child fragments |
| 104 | + // tslint:disable-next-line no-console |
| 105 | + console.warn('Fragments not supported on Mutation type for slicknode-apollo-link'); |
| 106 | + } |
| 107 | + }); |
| 108 | + |
| 109 | + fields.forEach((field) => { |
| 110 | + if (field.name.value === 'logoutUser') { |
| 111 | + // Subscribe to result to remove auth tokens from storage |
| 112 | + resultListeners.push(() => { |
| 113 | + this.debug('Removing auth tokens from storage'); |
| 114 | + this.logout(); |
| 115 | + }); |
| 116 | + } else if ( |
| 117 | + field.directives && |
| 118 | + field.directives.find((directive) => directive.name.value === 'authenticate') |
| 119 | + ) { |
| 120 | + const fieldName = field.alias ? field.alias.value : field.name.value; |
| 121 | + // Subscribe to result to set auth token set |
| 122 | + resultListeners.push((result) => { |
| 123 | + // Validate auth token set and update tokens if valid |
| 124 | + if ( |
| 125 | + result.data && |
| 126 | + result.data.hasOwnProperty(fieldName) && |
| 127 | + typeof result.data[fieldName] === 'object' |
| 128 | + ) { |
| 129 | + const tokenSet = result.data[fieldName]; |
| 130 | + if ( |
| 131 | + typeof tokenSet.accessToken === 'string' && |
| 132 | + typeof tokenSet.accessTokenLifetime === 'number' && |
| 133 | + typeof tokenSet.refreshToken === 'string' && |
| 134 | + typeof tokenSet.refreshTokenLifetime === 'number' |
| 135 | + ) { |
| 136 | + // Update auth tokens in storage of link |
| 137 | + this.setAuthTokenSet(tokenSet); |
| 138 | + this.debug('Login successful, auth token set updated'); |
| 139 | + } else { |
| 140 | + this.debug('The auth token set has no valid format'); |
| 141 | + } |
| 142 | + } else { |
| 143 | + this.debug('No valid token set returned'); |
| 144 | + } |
| 145 | + }); |
| 146 | + } |
| 147 | + }); |
| 148 | + } |
| 149 | + // Remove @authenticated directives from document |
| 150 | + operation.query = removeDirectivesFromDocument( |
| 151 | + [ authenticationDirectiveRemoveConfig ], |
| 152 | + operation.query, |
| 153 | + ); |
| 154 | + const nextObservable = forward(operation); |
| 155 | + |
| 156 | + // Add result listeners for token and logout processing |
| 157 | + resultListeners.map((listener) => nextObservable.subscribe(listener)); |
| 158 | + nextObservable.subscribe(observer); |
78 | 159 | }) |
79 | 160 | .catch((error) => { |
80 | 161 | this.debug('Error obtaining auth headers in SlicknodeLink'); |
81 | 162 | observer.error(error); |
82 | 163 | }); |
83 | | - |
84 | | - // return forward(operation); |
85 | | - |
86 | 164 | }); |
87 | 165 | } |
88 | 166 |
|
@@ -203,7 +281,7 @@ export default class SlicknodeLink extends ApolloLink { |
203 | 281 | /** |
204 | 282 | * Clears all tokens in the storage |
205 | 283 | */ |
206 | | - public async logout(forward: NextLink): Promise<void> { |
| 284 | + public async logout(): Promise<void> { |
207 | 285 | this.storage.removeItem(this.namespace + REFRESH_TOKEN_KEY); |
208 | 286 | this.storage.removeItem(this.namespace + REFRESH_TOKEN_EXPIRES_KEY); |
209 | 287 | this.storage.removeItem(this.namespace + ACCESS_TOKEN_KEY); |
|
0 commit comments