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";
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<string>();
const [accessToken, setAccessToken] = useState<string | null>(
localStorage.getItem("accessToken")
);
const [refreshToken, setRefreshToken] = useState<string>();
const [account, setAccount] = useState<any>();
@ -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}

View File

@ -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<HTMLIFrameElement>(null);
@ -41,11 +53,14 @@ export const Login: FC = () => {
const classes = useStyles();
return (
<iframe
ref={iframeRef}
className={classes.iframe}
title="Auth"
src="https://user.rpi.ivanli.cc/auth/login"
></iframe>
<Fragment>
<div className={classes.mask} />
<iframe
ref={iframeRef}
className={classes.iframe}
title="Auth"
src="https://user.rpi.ivanli.cc/auth/login"
></iframe>
</Fragment>
);
};

View File

@ -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<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 }) => {
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 <ApolloProvider client={client}>{children}</ApolloProvider>;
};

View File

@ -1,11 +1,8 @@
import { gql } from '@apollo/client';
export type Maybe<T> = T | null;
export type Exact<T extends { [key: string]: unknown }> = {
[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 Exact<T extends { [key: string]: unknown }> = { [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]> };
/** 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<PipelineTask>;
};
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<PipelineUnits>;
};
export type CreateProjectInput = {
name: Scalars["String"];
comment: Scalars["String"];
sshUrl: Scalars["String"];
webUrl?: Maybe<Scalars["String"]>;
webHookSecret?: Maybe<Scalars["String"]>;
name: Scalars['String'];
comment: Scalars['String'];
sshUrl: Scalars['String'];
webUrl?: Maybe<Scalars['String']>;
webHookSecret?: Maybe<Scalars['String']>;
};
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<PipelineTask>;
};
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<PipelineUnits>;
logs: Array<PipelineTaskLogs>;
status: TaskStatuses;
startedAt?: Maybe<Scalars["DateTime"]>;
endedAt?: Maybe<Scalars["DateTime"]>;
runOn: Scalars["String"];
startedAt?: Maybe<Scalars['DateTime']>;
endedAt?: Maybe<Scalars['DateTime']>;
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<PipelineUnits>;
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<Scalars["DateTime"]>;
endedAt?: Maybe<Scalars["DateTime"]>;
logs: Scalars["String"];
startedAt?: Maybe<Scalars['DateTime']>;
endedAt?: Maybe<Scalars['DateTime']>;
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<Scalars["String"]>;
webHookSecret?: Maybe<Scalars["String"]>;
__typename?: 'Project';
id: Scalars['ID'];
name: Scalars['String'];
comment: Scalars['String'];
sshUrl: Scalars['String'];
webUrl?: Maybe<Scalars['String']>;
webHookSecret?: Maybe<Scalars['String']>;
};
export type Query = {
__typename?: "Query";
__typename?: 'Query';
hello: Hello;
projects: Array<Project>;
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<Scalars["String"]>;
projectId?: Maybe<Scalars['String']>;
};
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<Scalars["String"]>;
__typename?: 'Subscription';
syncCommits?: Maybe<Scalars['String']>;
pipelineTaskEvent: PipelineTaskEvent;
pipelineTaskChanged: PipelineTask;
};
export type SubscriptionSyncCommitsArgs = {
appInstance?: Maybe<Scalars["String"]>;
pipelineId: Scalars["String"];
appInstance?: Maybe<Scalars['String']>;
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<Scalars["String"]>;
webHookSecret?: Maybe<Scalars["String"]>;
id: Scalars["String"];
name: Scalars['String'];
comment: Scalars['String'];
sshUrl: Scalars['String'];
webUrl?: Maybe<Scalars['String']>;
webHookSecret?: Maybe<Scalars['String']>;
id: Scalars['String'];
};
export type WorkUnit = {
__typename?: "WorkUnit";
__typename?: 'WorkUnit';
type: PipelineUnits;
scripts: Array<Scalars["String"]>;
scripts: Array<Scalars['String']>;
};
export type WorkUnitInput = {
type: PipelineUnits;
scripts: Array<Scalars["String"]>;
scripts: Array<Scalars['String']>;
};
export type WorkUnitMetadata = {
__typename?: "WorkUnitMetadata";
version: Scalars["Int"];
__typename?: 'WorkUnitMetadata';
version: Scalars['Int'];
units: Array<WorkUnit>;
};
export type WorkUnitMetadataInput = {
version?: Maybe<Scalars["Int"]>;
version?: Maybe<Scalars['Int']>;
units: Array<WorkUnitInput>;
};

View File

@ -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(
<React.StrictMode>
<ConfirmProvider>
<SnackbarProvider maxSnack={5}>
<FennecApolloClientProvider>
<MuiPickersUtilsProvider utils={DateFnsUtils} locale={zhLocale}>
<AuthProvider>
<AuthProvider>
<AppApolloClientProvider>
<MuiPickersUtilsProvider utils={DateFnsUtils} locale={zhLocale}>
<Router>
<App />
</Router>
</AuthProvider>
</MuiPickersUtilsProvider>
</FennecApolloClientProvider>
</MuiPickersUtilsProvider>
</AppApolloClientProvider>
</AuthProvider>
</SnackbarProvider>
</ConfirmProvider>
</React.StrictMode>,