feat: 添加鉴权相关逻辑。

This commit is contained in:
Ivan Li 2021-07-18 22:39:50 +08:00
parent 3644718b19
commit 4e493f79d1
5 changed files with 305 additions and 195 deletions

View File

@ -3,20 +3,23 @@ import { FC } from "react";
import { Login } from "./login"; import { Login } from "./login";
export interface AuthContext { export interface AuthContext {
accessToken: string | undefined; accessToken: string | null;
setAccessToken: (token: string) => void; setAccessToken: (token: string) => void;
setRefreshToken: (token: string) => void; setRefreshToken: (token: string) => void;
refreshToken: string | undefined; refreshToken: string | undefined;
login: (dto: any) => void; login: (dto: any) => void;
account?: any; account?: any;
setAccount: (dto: any) => void; setAccount: (dto: any) => void;
logout: () => void;
} }
const Context = createContext({} as AuthContext); const Context = createContext({} as AuthContext);
export const useAuth = () => useContext(Context); export const useAuth = () => useContext(Context);
export const AuthProvider: FC = ({ children }) => { export const AuthProvider: FC = ({ children }) => {
const [accessToken, setAccessToken] = useState<string>(); const [accessToken, setAccessToken] = useState<string | null>(
localStorage.getItem("accessToken")
);
const [refreshToken, setRefreshToken] = useState<string>(); const [refreshToken, setRefreshToken] = useState<string>();
const [account, setAccount] = useState<any>(); const [account, setAccount] = useState<any>();
@ -24,6 +27,12 @@ export const AuthProvider: FC = ({ children }) => {
setAccessToken(dto.accessToken); setAccessToken(dto.accessToken);
setRefreshToken(dto.refreshToken); setRefreshToken(dto.refreshToken);
setAccount(dto.account); setAccount(dto.account);
localStorage.setItem("accessToken", dto.accessToken);
};
const logout = () => {
setAccessToken(null);
setRefreshToken(undefined);
setAccount(undefined);
}; };
return ( return (
@ -36,6 +45,7 @@ export const AuthProvider: FC = ({ children }) => {
login, login,
account, account,
setAccount, setAccount,
logout,
}} }}
> >
{children} {children}

View File

@ -1,7 +1,7 @@
import { makeStyles } from "@material-ui/core"; import { makeStyles } from "@material-ui/core";
import { FC, useEffect, useRef } from "react"; import { FC, Fragment, useEffect, useRef } from "react";
import { useAuth } from "./auth.provider"; import { useAuth } from "./auth.provider";
const useStyles = makeStyles({ const useStyles = makeStyles((theme) => ({
iframe: { iframe: {
height: "300px", height: "300px",
width: "500px", width: "500px",
@ -9,8 +9,20 @@ const useStyles = makeStyles({
top: "100px", top: "100px",
left: "50%", left: "50%",
transform: "translateX(-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 = () => { export const Login: FC = () => {
const iframeRef = useRef<HTMLIFrameElement>(null); const iframeRef = useRef<HTMLIFrameElement>(null);
@ -41,11 +53,14 @@ export const Login: FC = () => {
const classes = useStyles(); const classes = useStyles();
return ( return (
<Fragment>
<div className={classes.mask} />
<iframe <iframe
ref={iframeRef} ref={iframeRef}
className={classes.iframe} className={classes.iframe}
title="Auth" title="Auth"
src="https://user.rpi.ivanli.cc/auth/login" src="https://user.rpi.ivanli.cc/auth/login"
></iframe> ></iframe>
</Fragment>
); );
}; };

View File

@ -5,6 +5,8 @@ import {
InMemoryCache, InMemoryCache,
split, split,
ApolloProvider, ApolloProvider,
fromPromise,
FetchResult,
} from "@apollo/client"; } from "@apollo/client";
import { withScalars } from "apollo-link-scalars"; import { withScalars } from "apollo-link-scalars";
import { buildClientSchema, IntrospectionQuery } from "graphql"; import { buildClientSchema, IntrospectionQuery } from "graphql";
@ -13,9 +15,16 @@ import { FC } from "react";
import introspectionResult from "../../generated/graphql.schema.json"; import introspectionResult from "../../generated/graphql.schema.json";
import { onError } from "@apollo/client/link/error"; import { onError } from "@apollo/client/link/error";
import { WebSocketLink } from "@apollo/client/link/ws"; 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 { useSnackbar } from "notistack";
import { deepOmit } from "../../utils/deep-omit"; 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( const schema = buildClientSchema(
introspectionResult as unknown as IntrospectionQuery introspectionResult as unknown as IntrospectionQuery
@ -37,8 +46,73 @@ const cleanTypeName = new ApolloLink((operation, forward) => {
); );
}); });
export const FennecApolloClientProvider: FC = ({ children }) => { export const AppApolloClientProvider: FC = ({ children }) => {
const { enqueueSnackbar } = useSnackbar(); const { enqueueSnackbar } = useSnackbar();
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<Observable<FetchResult>>((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<Observable<FetchResult>>((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 }) => { const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) { if (graphQLErrors) {
graphQLErrors.forEach((error) => { graphQLErrors.forEach((error) => {
@ -65,6 +139,11 @@ export const FennecApolloClientProvider: FC = ({ children }) => {
}:${window.location.port}/api/graphql`, }:${window.location.port}/api/graphql`,
options: { options: {
reconnect: true, reconnect: true,
connectionParams: {
headers: {
authorization: `Bearer ${accessTokenRef.current} `,
},
},
}, },
}); });
const httpLink = new HttpLink({ const httpLink = new HttpLink({
@ -83,6 +162,7 @@ export const FennecApolloClientProvider: FC = ({ children }) => {
); );
const link = ApolloLink.from([ const link = ApolloLink.from([
errorLink, errorLink,
authLink,
withScalars({ schema, typesMap }) as unknown as ApolloLink, withScalars({ schema, typesMap }) as unknown as ApolloLink,
cleanTypeName, cleanTypeName,
splitLink, splitLink,
@ -92,22 +172,12 @@ export const FennecApolloClientProvider: FC = ({ children }) => {
ssrMode: typeof window === "undefined", ssrMode: typeof window === "undefined",
link, link,
cache: new InMemoryCache({ cache: new InMemoryCache({
typePolicies: { typePolicies: {},
// PipelineTaskLogs: {
// keyFields: ["unit"],
// },
// PipelineTask: {
// fields: {
// logs: {
// merge(existing = [], incoming: any[]) {
// return [...existing, ...incoming];
// },
// },
// },
// },
},
}), }),
}); });
return client;
}, [enqueueSnackbar, loggedEventTarget]);
return <ApolloProvider client={client}>{children}</ApolloProvider>; return <ApolloProvider client={client}>{children}</ApolloProvider>;
}; };

View File

@ -1,11 +1,8 @@
import { gql } from '@apollo/client';
export type Maybe<T> = T | null; export type Maybe<T> = T | null;
export type Exact<T extends { [key: string]: unknown }> = { export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
[K in keyof T]: T[K]; export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
}; export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> &
{ [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> &
{ [SubKey in K]: Maybe<T[SubKey]> };
/** All built-in and custom scalars, mapped to their actual values */ /** All built-in and custom scalars, mapped to their actual values */
export type Scalars = { export type Scalars = {
ID: string; ID: string;
@ -18,165 +15,174 @@ export type Scalars = {
}; };
export type Commit = { export type Commit = {
__typename?: "Commit"; __typename?: 'Commit';
hash: Scalars["String"]; hash: Scalars['String'];
date: Scalars["DateTime"]; date: Scalars['DateTime'];
message: Scalars["String"]; message: Scalars['String'];
refs: Scalars["String"]; refs: Scalars['String'];
body: Scalars["String"]; body: Scalars['String'];
author_name: Scalars["String"]; author_name: Scalars['String'];
author_email: Scalars["String"]; author_email: Scalars['String'];
tasks: Array<PipelineTask>; tasks: Array<PipelineTask>;
}; };
export type CreatePipelineInput = { export type CreatePipelineInput = {
projectId: Scalars["String"]; projectId: Scalars['String'];
branch: Scalars["String"]; branch: Scalars['String'];
name: Scalars["String"]; name: Scalars['String'];
workUnitMetadata: WorkUnitMetadataInput; workUnitMetadata: WorkUnitMetadataInput;
}; };
export type CreatePipelineTaskInput = { export type CreatePipelineTaskInput = {
pipelineId: Scalars["String"]; pipelineId: Scalars['String'];
commit: Scalars["String"]; commit: Scalars['String'];
units: Array<PipelineUnits>; units: Array<PipelineUnits>;
}; };
export type CreateProjectInput = { export type CreateProjectInput = {
name: Scalars["String"]; name: Scalars['String'];
comment: Scalars["String"]; comment: Scalars['String'];
sshUrl: Scalars["String"]; sshUrl: Scalars['String'];
webUrl?: Maybe<Scalars["String"]>; webUrl?: Maybe<Scalars['String']>;
webHookSecret?: Maybe<Scalars["String"]>; webHookSecret?: Maybe<Scalars['String']>;
}; };
export type Hello = { export type Hello = {
__typename?: "Hello"; __typename?: 'Hello';
message: Scalars["String"]; message: Scalars['String'];
}; };
export type LogFields = { export type LogFields = {
__typename?: "LogFields"; __typename?: 'LogFields';
hash: Scalars["String"]; hash: Scalars['String'];
date: Scalars["String"]; date: Scalars['String'];
message: Scalars["String"]; message: Scalars['String'];
refs: Scalars["String"]; refs: Scalars['String'];
body: Scalars["String"]; body: Scalars['String'];
author_name: Scalars["String"]; author_name: Scalars['String'];
author_email: Scalars["String"]; author_email: Scalars['String'];
tasks: Array<PipelineTask>; tasks: Array<PipelineTask>;
}; };
export type Mutation = { export type Mutation = {
__typename?: "Mutation"; __typename?: 'Mutation';
createProject: Project; createProject: Project;
updateProject: Project; updateProject: Project;
removeProject: Scalars["Float"]; removeProject: Scalars['Float'];
createPipeline: Pipeline; createPipeline: Pipeline;
updatePipeline: Pipeline; updatePipeline: Pipeline;
deletePipeline: Scalars["Float"]; deletePipeline: Scalars['Float'];
createPipelineTask: PipelineTask; createPipelineTask: PipelineTask;
stopPipelineTask: Scalars["Boolean"]; stopPipelineTask: Scalars['Boolean'];
}; };
export type MutationCreateProjectArgs = { export type MutationCreateProjectArgs = {
project: CreateProjectInput; project: CreateProjectInput;
}; };
export type MutationUpdateProjectArgs = { export type MutationUpdateProjectArgs = {
project: UpdateProjectInput; project: UpdateProjectInput;
}; };
export type MutationRemoveProjectArgs = { export type MutationRemoveProjectArgs = {
id: Scalars["String"]; id: Scalars['String'];
}; };
export type MutationCreatePipelineArgs = { export type MutationCreatePipelineArgs = {
pipeline: CreatePipelineInput; pipeline: CreatePipelineInput;
}; };
export type MutationUpdatePipelineArgs = { export type MutationUpdatePipelineArgs = {
pipeline: UpdatePipelineInput; pipeline: UpdatePipelineInput;
}; };
export type MutationDeletePipelineArgs = { export type MutationDeletePipelineArgs = {
id: Scalars["String"]; id: Scalars['String'];
}; };
export type MutationCreatePipelineTaskArgs = { export type MutationCreatePipelineTaskArgs = {
task: CreatePipelineTaskInput; task: CreatePipelineTaskInput;
}; };
export type MutationStopPipelineTaskArgs = { export type MutationStopPipelineTaskArgs = {
id: Scalars["String"]; id: Scalars['String'];
}; };
export type Pipeline = { export type Pipeline = {
__typename?: "Pipeline"; __typename?: 'Pipeline';
id: Scalars["ID"]; id: Scalars['ID'];
project: Project; project: Project;
projectId: Scalars["String"]; projectId: Scalars['String'];
branch: Scalars["String"]; branch: Scalars['String'];
name: Scalars["String"]; name: Scalars['String'];
workUnitMetadata: WorkUnitMetadata; workUnitMetadata: WorkUnitMetadata;
}; };
export type PipelineTask = { export type PipelineTask = {
__typename?: "PipelineTask"; __typename?: 'PipelineTask';
id: Scalars["ID"]; id: Scalars['ID'];
pipeline: Pipeline; pipeline: Pipeline;
pipelineId: Scalars["String"]; pipelineId: Scalars['String'];
commit: Scalars["String"]; commit: Scalars['String'];
units: Array<PipelineUnits>; units: Array<PipelineUnits>;
logs: Array<PipelineTaskLogs>; logs: Array<PipelineTaskLogs>;
status: TaskStatuses; status: TaskStatuses;
startedAt?: Maybe<Scalars["DateTime"]>; startedAt?: Maybe<Scalars['DateTime']>;
endedAt?: Maybe<Scalars["DateTime"]>; endedAt?: Maybe<Scalars['DateTime']>;
runOn: Scalars["String"]; runOn: Scalars['String'];
}; };
export type PipelineTaskEvent = { export type PipelineTaskEvent = {
__typename?: "PipelineTaskEvent"; __typename?: 'PipelineTaskEvent';
taskId: Scalars["String"]; taskId: Scalars['String'];
pipelineId: Scalars["String"]; pipelineId: Scalars['String'];
projectId: Scalars["String"]; projectId: Scalars['String'];
unit?: Maybe<PipelineUnits>; unit?: Maybe<PipelineUnits>;
emittedAt: Scalars["DateTime"]; emittedAt: Scalars['DateTime'];
message: Scalars["String"]; message: Scalars['String'];
messageType: Scalars["String"]; messageType: Scalars['String'];
status: TaskStatuses; status: TaskStatuses;
}; };
export type PipelineTaskLogs = { export type PipelineTaskLogs = {
__typename?: "PipelineTaskLogs"; __typename?: 'PipelineTaskLogs';
unit: PipelineUnits; unit: PipelineUnits;
status: TaskStatuses; status: TaskStatuses;
startedAt?: Maybe<Scalars["DateTime"]>; startedAt?: Maybe<Scalars['DateTime']>;
endedAt?: Maybe<Scalars["DateTime"]>; endedAt?: Maybe<Scalars['DateTime']>;
logs: Scalars["String"]; logs: Scalars['String'];
}; };
/** 流水线单元 */ /** 流水线单元 */
export enum PipelineUnits { export enum PipelineUnits {
Checkout = "checkout", Checkout = 'checkout',
InstallDependencies = "installDependencies", InstallDependencies = 'installDependencies',
Test = "test", Test = 'test',
Deploy = "deploy", Deploy = 'deploy',
CleanUp = "cleanUp", CleanUp = 'cleanUp'
} }
export type Project = { export type Project = {
__typename?: "Project"; __typename?: 'Project';
id: Scalars["ID"]; id: Scalars['ID'];
name: Scalars["String"]; name: Scalars['String'];
comment: Scalars["String"]; comment: Scalars['String'];
sshUrl: Scalars["String"]; sshUrl: Scalars['String'];
webUrl?: Maybe<Scalars["String"]>; webUrl?: Maybe<Scalars['String']>;
webHookSecret?: Maybe<Scalars["String"]>; webHookSecret?: Maybe<Scalars['String']>;
}; };
export type Query = { export type Query = {
__typename?: "Query"; __typename?: 'Query';
hello: Hello; hello: Hello;
projects: Array<Project>; projects: Array<Project>;
project: Project; project: Project;
@ -187,92 +193,101 @@ export type Query = {
pipelineTask: PipelineTask; pipelineTask: PipelineTask;
}; };
export type QueryProjectArgs = { export type QueryProjectArgs = {
id: Scalars["String"]; id: Scalars['String'];
}; };
export type QueryPipelinesArgs = { export type QueryPipelinesArgs = {
projectId?: Maybe<Scalars["String"]>; projectId?: Maybe<Scalars['String']>;
}; };
export type QueryPipelineArgs = { export type QueryPipelineArgs = {
id: Scalars["String"]; id: Scalars['String'];
}; };
export type QueryCommitsArgs = { export type QueryCommitsArgs = {
pipelineId: Scalars["String"]; pipelineId: Scalars['String'];
}; };
export type QueryListPipelineTaskByPipelineIdArgs = { export type QueryListPipelineTaskByPipelineIdArgs = {
pipelineId: Scalars["String"]; pipelineId: Scalars['String'];
}; };
export type QueryPipelineTaskArgs = { export type QueryPipelineTaskArgs = {
id: Scalars["String"]; id: Scalars['String'];
}; };
export type Subscription = { export type Subscription = {
__typename?: "Subscription"; __typename?: 'Subscription';
syncCommits?: Maybe<Scalars["String"]>; syncCommits?: Maybe<Scalars['String']>;
pipelineTaskEvent: PipelineTaskEvent; pipelineTaskEvent: PipelineTaskEvent;
pipelineTaskChanged: PipelineTask; pipelineTaskChanged: PipelineTask;
}; };
export type SubscriptionSyncCommitsArgs = { export type SubscriptionSyncCommitsArgs = {
appInstance?: Maybe<Scalars["String"]>; appInstance?: Maybe<Scalars['String']>;
pipelineId: Scalars["String"]; pipelineId: Scalars['String'];
}; };
export type SubscriptionPipelineTaskEventArgs = { export type SubscriptionPipelineTaskEventArgs = {
taskId: Scalars["String"]; taskId: Scalars['String'];
}; };
export type SubscriptionPipelineTaskChangedArgs = { export type SubscriptionPipelineTaskChangedArgs = {
id: Scalars["String"]; id: Scalars['String'];
}; };
/** 任务状态 */ /** 任务状态 */
export enum TaskStatuses { export enum TaskStatuses {
Success = "success", Success = 'success',
Failed = "failed", Failed = 'failed',
Working = "working", Working = 'working',
Pending = "pending", Pending = 'pending'
} }
export type UpdatePipelineInput = { export type UpdatePipelineInput = {
branch: Scalars["String"]; branch: Scalars['String'];
name: Scalars["String"]; name: Scalars['String'];
workUnitMetadata: WorkUnitMetadataInput; workUnitMetadata: WorkUnitMetadataInput;
id: Scalars["String"]; id: Scalars['String'];
}; };
export type UpdateProjectInput = { export type UpdateProjectInput = {
name: Scalars["String"]; name: Scalars['String'];
comment: Scalars["String"]; comment: Scalars['String'];
sshUrl: Scalars["String"]; sshUrl: Scalars['String'];
webUrl?: Maybe<Scalars["String"]>; webUrl?: Maybe<Scalars['String']>;
webHookSecret?: Maybe<Scalars["String"]>; webHookSecret?: Maybe<Scalars['String']>;
id: Scalars["String"]; id: Scalars['String'];
}; };
export type WorkUnit = { export type WorkUnit = {
__typename?: "WorkUnit"; __typename?: 'WorkUnit';
type: PipelineUnits; type: PipelineUnits;
scripts: Array<Scalars["String"]>; scripts: Array<Scalars['String']>;
}; };
export type WorkUnitInput = { export type WorkUnitInput = {
type: PipelineUnits; type: PipelineUnits;
scripts: Array<Scalars["String"]>; scripts: Array<Scalars['String']>;
}; };
export type WorkUnitMetadata = { export type WorkUnitMetadata = {
__typename?: "WorkUnitMetadata"; __typename?: 'WorkUnitMetadata';
version: Scalars["Int"]; version: Scalars['Int'];
units: Array<WorkUnit>; units: Array<WorkUnit>;
}; };
export type WorkUnitMetadataInput = { export type WorkUnitMetadataInput = {
version?: Maybe<Scalars["Int"]>; version?: Maybe<Scalars['Int']>;
units: Array<WorkUnitInput>; units: Array<WorkUnitInput>;
}; };

View File

@ -4,7 +4,7 @@ import "./index.css";
import "fontsource-roboto"; import "fontsource-roboto";
import App from "./App"; import App from "./App";
import reportWebVitals from "./reportWebVitals"; import reportWebVitals from "./reportWebVitals";
import { FennecApolloClientProvider } from "./commons/graphql/client"; import { AppApolloClientProvider } from "./commons/graphql/client";
import { MuiPickersUtilsProvider } from "@material-ui/pickers"; import { MuiPickersUtilsProvider } from "@material-ui/pickers";
import DateFnsUtils from "@date-io/date-fns"; import DateFnsUtils from "@date-io/date-fns";
import zhLocale from "date-fns/locale/zh-CN"; import zhLocale from "date-fns/locale/zh-CN";
@ -17,15 +17,15 @@ ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<ConfirmProvider> <ConfirmProvider>
<SnackbarProvider maxSnack={5}> <SnackbarProvider maxSnack={5}>
<FennecApolloClientProvider>
<MuiPickersUtilsProvider utils={DateFnsUtils} locale={zhLocale}>
<AuthProvider> <AuthProvider>
<AppApolloClientProvider>
<MuiPickersUtilsProvider utils={DateFnsUtils} locale={zhLocale}>
<Router> <Router>
<App /> <App />
</Router> </Router>
</AuthProvider>
</MuiPickersUtilsProvider> </MuiPickersUtilsProvider>
</FennecApolloClientProvider> </AppApolloClientProvider>
</AuthProvider>
</SnackbarProvider> </SnackbarProvider>
</ConfirmProvider> </ConfirmProvider>
</React.StrictMode>, </React.StrictMode>,