diff --git a/src/commons/auth/auth.provider.tsx b/src/commons/auth/auth.provider.tsx index ac97b6e..e9366d8 100644 --- a/src/commons/auth/auth.provider.tsx +++ b/src/commons/auth/auth.provider.tsx @@ -3,20 +3,23 @@ import { FC } from "react"; import { Login } from "./login"; export interface AuthContext { - accessToken: string | undefined; + accessToken: string | null; setAccessToken: (token: string) => void; setRefreshToken: (token: string) => void; refreshToken: string | undefined; login: (dto: any) => void; account?: any; setAccount: (dto: any) => void; + logout: () => void; } const Context = createContext({} as AuthContext); export const useAuth = () => useContext(Context); export const AuthProvider: FC = ({ children }) => { - const [accessToken, setAccessToken] = useState(); + const [accessToken, setAccessToken] = useState( + localStorage.getItem("accessToken") + ); const [refreshToken, setRefreshToken] = useState(); const [account, setAccount] = useState(); @@ -24,6 +27,12 @@ export const AuthProvider: FC = ({ children }) => { setAccessToken(dto.accessToken); setRefreshToken(dto.refreshToken); setAccount(dto.account); + localStorage.setItem("accessToken", dto.accessToken); + }; + const logout = () => { + setAccessToken(null); + setRefreshToken(undefined); + setAccount(undefined); }; return ( @@ -36,6 +45,7 @@ export const AuthProvider: FC = ({ children }) => { login, account, setAccount, + logout, }} > {children} diff --git a/src/commons/auth/login.tsx b/src/commons/auth/login.tsx index 9f3f773..7260adb 100644 --- a/src/commons/auth/login.tsx +++ b/src/commons/auth/login.tsx @@ -1,7 +1,7 @@ import { makeStyles } from "@material-ui/core"; -import { FC, useEffect, useRef } from "react"; +import { FC, Fragment, useEffect, useRef } from "react"; import { useAuth } from "./auth.provider"; -const useStyles = makeStyles({ +const useStyles = makeStyles((theme) => ({ iframe: { height: "300px", width: "500px", @@ -9,8 +9,20 @@ const useStyles = makeStyles({ top: "100px", left: "50%", transform: "translateX(-50%)", + zIndex: theme.zIndex.modal, + border: "none", + boxShadow: theme.shadows[4], }, -}); + mask: { + top: "0", + left: "0", + bottom: "0", + right: "0", + position: "absolute", + backgroundColor: "rgba(0, 0, 0, 0.3)", + zIndex: theme.zIndex.modal, + }, +})); export const Login: FC = () => { const iframeRef = useRef(null); @@ -41,11 +53,14 @@ export const Login: FC = () => { const classes = useStyles(); return ( - + +
+ + ); }; diff --git a/src/commons/graphql/client.tsx b/src/commons/graphql/client.tsx index c0c5463..73da9fe 100644 --- a/src/commons/graphql/client.tsx +++ b/src/commons/graphql/client.tsx @@ -5,6 +5,8 @@ import { InMemoryCache, split, ApolloProvider, + fromPromise, + FetchResult, } from "@apollo/client"; import { withScalars } from "apollo-link-scalars"; import { buildClientSchema, IntrospectionQuery } from "graphql"; @@ -13,9 +15,16 @@ import { FC } from "react"; import introspectionResult from "../../generated/graphql.schema.json"; import { onError } from "@apollo/client/link/error"; import { WebSocketLink } from "@apollo/client/link/ws"; -import { getMainDefinition } from "@apollo/client/utilities"; +import { getMainDefinition, Observable } from "@apollo/client/utilities"; import { useSnackbar } from "notistack"; import { deepOmit } from "../../utils/deep-omit"; +import { useMemo } from "react"; +import { EventEmitter } from "events"; +import { useState } from "react"; +import { useAuth } from "../auth/auth.provider"; +import { useEffect } from "react"; +import { useRef } from "react"; +import { setContext } from "@apollo/client/link/context"; const schema = buildClientSchema( introspectionResult as unknown as IntrospectionQuery @@ -37,77 +46,138 @@ const cleanTypeName = new ApolloLink((operation, forward) => { ); }); -export const FennecApolloClientProvider: FC = ({ children }) => { +export const AppApolloClientProvider: FC = ({ children }) => { const { enqueueSnackbar } = useSnackbar(); - const errorLink = onError(({ graphQLErrors, networkError }) => { - if (graphQLErrors) { - graphQLErrors.forEach((error) => { - enqueueSnackbar(error.message, { + const { accessToken, logout } = useAuth(); + + const [loggedEventTarget] = useState(() => new EventEmitter()); + const accessTokenRef = useRef(accessToken); + const logoutRef = useRef(logout); + + useEffect(() => { + accessTokenRef.current = accessToken; + if (accessToken) { + loggedEventTarget.emit("logged", accessToken); + } + }, [loggedEventTarget, accessToken]); + useEffect(() => { + logoutRef.current = logout; + }, [logout]); + const client = useMemo(() => { + const authLink = onError( + ({ graphQLErrors, networkError, operation, forward }) => { + if (graphQLErrors) { + for (const error of graphQLErrors) { + if (error.extensions?.code === "401") { + return fromPromise( + new Promise>((resolve) => { + loggedEventTarget.once("logged", (accessToken: string) => { + const oldHeaders = operation.getContext().headers; + operation.setContext({ + headers: { + ...oldHeaders, + authorization: `Bearer ${accessToken}`, + }, + }); + resolve(forward(operation)); + }); + logoutRef.current(); + }) + ).flatMap((v) => v); + } + } + } + const httpResult = (networkError as any)?.result; + if (httpResult?.statusCode === 401) { + return fromPromise( + new Promise>((resolve) => { + loggedEventTarget.once("logged", (accessToken: string) => { + const oldHeaders = operation.getContext().headers; + operation.setContext({ + headers: { + ...oldHeaders, + authorization: `Bearer ${accessToken}`, + }, + }); + resolve(forward(operation)); + }); + logoutRef.current(); + }) + ).flatMap((v) => v); + } + } + ).concat( + setContext(() => ({ + headers: { + authorization: `Bearer ${accessTokenRef.current}`, + }, + })) + ); + const errorLink = onError(({ graphQLErrors, networkError }) => { + if (graphQLErrors) { + graphQLErrors.forEach((error) => { + enqueueSnackbar(error.message, { + variant: "error", + }); + }); + graphQLErrors.forEach(({ message, locations, path }) => { + console.error( + `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}` + ); + }); + } + if (networkError) { + console.log(`[Network error]: ${networkError}`); + enqueueSnackbar(networkError.message, { variant: "error", }); - }); - graphQLErrors.forEach(({ message, locations, path }) => { - console.error( - `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}` - ); - }); - } - if (networkError) { - console.log(`[Network error]: ${networkError}`); - enqueueSnackbar(networkError.message, { - variant: "error", - }); - } - }); - const wsLink = new WebSocketLink({ - uri: `${window.location.protocol.replace("http", "ws")}//${ - window.location.hostname - }:${window.location.port}/api/graphql`, - options: { - reconnect: true, - }, - }); - const httpLink = new HttpLink({ - uri: "/api/graphql", - }); - const splitLink = split( - ({ query }) => { - const definition = getMainDefinition(query); - return ( - definition.kind === "OperationDefinition" && - definition.operation === "subscription" - ); - }, - wsLink, - httpLink - ); - const link = ApolloLink.from([ - errorLink, - withScalars({ schema, typesMap }) as unknown as ApolloLink, - cleanTypeName, - splitLink, - ]); - const client = new ApolloClient({ - connectToDevTools: true, - ssrMode: typeof window === "undefined", - link, - cache: new InMemoryCache({ - typePolicies: { - // PipelineTaskLogs: { - // keyFields: ["unit"], - // }, - // PipelineTask: { - // fields: { - // logs: { - // merge(existing = [], incoming: any[]) { - // return [...existing, ...incoming]; - // }, - // }, - // }, - // }, + } + }); + const wsLink = new WebSocketLink({ + uri: `${window.location.protocol.replace("http", "ws")}//${ + window.location.hostname + }:${window.location.port}/api/graphql`, + options: { + reconnect: true, + connectionParams: { + headers: { + authorization: `Bearer ${accessTokenRef.current} `, + }, + }, }, - }), - }); + }); + const httpLink = new HttpLink({ + uri: "/api/graphql", + }); + const splitLink = split( + ({ query }) => { + const definition = getMainDefinition(query); + return ( + definition.kind === "OperationDefinition" && + definition.operation === "subscription" + ); + }, + wsLink, + httpLink + ); + const link = ApolloLink.from([ + errorLink, + authLink, + withScalars({ schema, typesMap }) as unknown as ApolloLink, + cleanTypeName, + splitLink, + ]); + const client = new ApolloClient({ + connectToDevTools: true, + ssrMode: typeof window === "undefined", + link, + cache: new InMemoryCache({ + typePolicies: {}, + }), + }); + + return client; + }, [enqueueSnackbar, loggedEventTarget]); return {children}; }; diff --git a/src/generated/graphql.tsx b/src/generated/graphql.tsx index 847cf5f..736d229 100644 --- a/src/generated/graphql.tsx +++ b/src/generated/graphql.tsx @@ -1,11 +1,8 @@ +import { gql } from '@apollo/client'; export type Maybe = T | null; -export type Exact = { - [K in keyof T]: T[K]; -}; -export type MakeOptional = Omit & - { [SubKey in K]?: Maybe }; -export type MakeMaybe = Omit & - { [SubKey in K]: Maybe }; +export type Exact = { [K in keyof T]: T[K] }; +export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; +export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -18,165 +15,174 @@ export type Scalars = { }; export type Commit = { - __typename?: "Commit"; - hash: Scalars["String"]; - date: Scalars["DateTime"]; - message: Scalars["String"]; - refs: Scalars["String"]; - body: Scalars["String"]; - author_name: Scalars["String"]; - author_email: Scalars["String"]; + __typename?: 'Commit'; + hash: Scalars['String']; + date: Scalars['DateTime']; + message: Scalars['String']; + refs: Scalars['String']; + body: Scalars['String']; + author_name: Scalars['String']; + author_email: Scalars['String']; tasks: Array; }; export type CreatePipelineInput = { - projectId: Scalars["String"]; - branch: Scalars["String"]; - name: Scalars["String"]; + projectId: Scalars['String']; + branch: Scalars['String']; + name: Scalars['String']; workUnitMetadata: WorkUnitMetadataInput; }; export type CreatePipelineTaskInput = { - pipelineId: Scalars["String"]; - commit: Scalars["String"]; + pipelineId: Scalars['String']; + commit: Scalars['String']; units: Array; }; export type CreateProjectInput = { - name: Scalars["String"]; - comment: Scalars["String"]; - sshUrl: Scalars["String"]; - webUrl?: Maybe; - webHookSecret?: Maybe; + name: Scalars['String']; + comment: Scalars['String']; + sshUrl: Scalars['String']; + webUrl?: Maybe; + webHookSecret?: Maybe; }; + export type Hello = { - __typename?: "Hello"; - message: Scalars["String"]; + __typename?: 'Hello'; + message: Scalars['String']; }; export type LogFields = { - __typename?: "LogFields"; - hash: Scalars["String"]; - date: Scalars["String"]; - message: Scalars["String"]; - refs: Scalars["String"]; - body: Scalars["String"]; - author_name: Scalars["String"]; - author_email: Scalars["String"]; + __typename?: 'LogFields'; + hash: Scalars['String']; + date: Scalars['String']; + message: Scalars['String']; + refs: Scalars['String']; + body: Scalars['String']; + author_name: Scalars['String']; + author_email: Scalars['String']; tasks: Array; }; export type Mutation = { - __typename?: "Mutation"; + __typename?: 'Mutation'; createProject: Project; updateProject: Project; - removeProject: Scalars["Float"]; + removeProject: Scalars['Float']; createPipeline: Pipeline; updatePipeline: Pipeline; - deletePipeline: Scalars["Float"]; + deletePipeline: Scalars['Float']; createPipelineTask: PipelineTask; - stopPipelineTask: Scalars["Boolean"]; + stopPipelineTask: Scalars['Boolean']; }; + export type MutationCreateProjectArgs = { project: CreateProjectInput; }; + export type MutationUpdateProjectArgs = { project: UpdateProjectInput; }; + export type MutationRemoveProjectArgs = { - id: Scalars["String"]; + id: Scalars['String']; }; + export type MutationCreatePipelineArgs = { pipeline: CreatePipelineInput; }; + export type MutationUpdatePipelineArgs = { pipeline: UpdatePipelineInput; }; + export type MutationDeletePipelineArgs = { - id: Scalars["String"]; + id: Scalars['String']; }; + export type MutationCreatePipelineTaskArgs = { task: CreatePipelineTaskInput; }; + export type MutationStopPipelineTaskArgs = { - id: Scalars["String"]; + id: Scalars['String']; }; export type Pipeline = { - __typename?: "Pipeline"; - id: Scalars["ID"]; + __typename?: 'Pipeline'; + id: Scalars['ID']; project: Project; - projectId: Scalars["String"]; - branch: Scalars["String"]; - name: Scalars["String"]; + projectId: Scalars['String']; + branch: Scalars['String']; + name: Scalars['String']; workUnitMetadata: WorkUnitMetadata; }; export type PipelineTask = { - __typename?: "PipelineTask"; - id: Scalars["ID"]; + __typename?: 'PipelineTask'; + id: Scalars['ID']; pipeline: Pipeline; - pipelineId: Scalars["String"]; - commit: Scalars["String"]; + pipelineId: Scalars['String']; + commit: Scalars['String']; units: Array; logs: Array; status: TaskStatuses; - startedAt?: Maybe; - endedAt?: Maybe; - runOn: Scalars["String"]; + startedAt?: Maybe; + endedAt?: Maybe; + runOn: Scalars['String']; }; export type PipelineTaskEvent = { - __typename?: "PipelineTaskEvent"; - taskId: Scalars["String"]; - pipelineId: Scalars["String"]; - projectId: Scalars["String"]; + __typename?: 'PipelineTaskEvent'; + taskId: Scalars['String']; + pipelineId: Scalars['String']; + projectId: Scalars['String']; unit?: Maybe; - emittedAt: Scalars["DateTime"]; - message: Scalars["String"]; - messageType: Scalars["String"]; + emittedAt: Scalars['DateTime']; + message: Scalars['String']; + messageType: Scalars['String']; status: TaskStatuses; }; export type PipelineTaskLogs = { - __typename?: "PipelineTaskLogs"; + __typename?: 'PipelineTaskLogs'; unit: PipelineUnits; status: TaskStatuses; - startedAt?: Maybe; - endedAt?: Maybe; - logs: Scalars["String"]; + startedAt?: Maybe; + endedAt?: Maybe; + logs: Scalars['String']; }; /** 流水线单元 */ export enum PipelineUnits { - Checkout = "checkout", - InstallDependencies = "installDependencies", - Test = "test", - Deploy = "deploy", - CleanUp = "cleanUp", + Checkout = 'checkout', + InstallDependencies = 'installDependencies', + Test = 'test', + Deploy = 'deploy', + CleanUp = 'cleanUp' } export type Project = { - __typename?: "Project"; - id: Scalars["ID"]; - name: Scalars["String"]; - comment: Scalars["String"]; - sshUrl: Scalars["String"]; - webUrl?: Maybe; - webHookSecret?: Maybe; + __typename?: 'Project'; + id: Scalars['ID']; + name: Scalars['String']; + comment: Scalars['String']; + sshUrl: Scalars['String']; + webUrl?: Maybe; + webHookSecret?: Maybe; }; export type Query = { - __typename?: "Query"; + __typename?: 'Query'; hello: Hello; projects: Array; project: Project; @@ -187,92 +193,101 @@ export type Query = { pipelineTask: PipelineTask; }; + export type QueryProjectArgs = { - id: Scalars["String"]; + id: Scalars['String']; }; + export type QueryPipelinesArgs = { - projectId?: Maybe; + projectId?: Maybe; }; + export type QueryPipelineArgs = { - id: Scalars["String"]; + id: Scalars['String']; }; + export type QueryCommitsArgs = { - pipelineId: Scalars["String"]; + pipelineId: Scalars['String']; }; + export type QueryListPipelineTaskByPipelineIdArgs = { - pipelineId: Scalars["String"]; + pipelineId: Scalars['String']; }; + export type QueryPipelineTaskArgs = { - id: Scalars["String"]; + id: Scalars['String']; }; export type Subscription = { - __typename?: "Subscription"; - syncCommits?: Maybe; + __typename?: 'Subscription'; + syncCommits?: Maybe; pipelineTaskEvent: PipelineTaskEvent; pipelineTaskChanged: PipelineTask; }; + export type SubscriptionSyncCommitsArgs = { - appInstance?: Maybe; - pipelineId: Scalars["String"]; + appInstance?: Maybe; + pipelineId: Scalars['String']; }; + export type SubscriptionPipelineTaskEventArgs = { - taskId: Scalars["String"]; + taskId: Scalars['String']; }; + export type SubscriptionPipelineTaskChangedArgs = { - id: Scalars["String"]; + id: Scalars['String']; }; /** 任务状态 */ export enum TaskStatuses { - Success = "success", - Failed = "failed", - Working = "working", - Pending = "pending", + Success = 'success', + Failed = 'failed', + Working = 'working', + Pending = 'pending' } export type UpdatePipelineInput = { - branch: Scalars["String"]; - name: Scalars["String"]; + branch: Scalars['String']; + name: Scalars['String']; workUnitMetadata: WorkUnitMetadataInput; - id: Scalars["String"]; + id: Scalars['String']; }; export type UpdateProjectInput = { - name: Scalars["String"]; - comment: Scalars["String"]; - sshUrl: Scalars["String"]; - webUrl?: Maybe; - webHookSecret?: Maybe; - id: Scalars["String"]; + name: Scalars['String']; + comment: Scalars['String']; + sshUrl: Scalars['String']; + webUrl?: Maybe; + webHookSecret?: Maybe; + id: Scalars['String']; }; export type WorkUnit = { - __typename?: "WorkUnit"; + __typename?: 'WorkUnit'; type: PipelineUnits; - scripts: Array; + scripts: Array; }; export type WorkUnitInput = { type: PipelineUnits; - scripts: Array; + scripts: Array; }; export type WorkUnitMetadata = { - __typename?: "WorkUnitMetadata"; - version: Scalars["Int"]; + __typename?: 'WorkUnitMetadata'; + version: Scalars['Int']; units: Array; }; export type WorkUnitMetadataInput = { - version?: Maybe; + version?: Maybe; units: Array; }; diff --git a/src/index.tsx b/src/index.tsx index 742e799..7d6682c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,7 +4,7 @@ import "./index.css"; import "fontsource-roboto"; import App from "./App"; import reportWebVitals from "./reportWebVitals"; -import { FennecApolloClientProvider } from "./commons/graphql/client"; +import { AppApolloClientProvider } from "./commons/graphql/client"; import { MuiPickersUtilsProvider } from "@material-ui/pickers"; import DateFnsUtils from "@date-io/date-fns"; import zhLocale from "date-fns/locale/zh-CN"; @@ -17,15 +17,15 @@ ReactDOM.render( - - - + + + - - - + + + ,