refactor: refactor webui

This commit is contained in:
2025-04-26 01:43:23 +08:00
parent b20f7cd1ad
commit ee1b1ae5e6
104 changed files with 934 additions and 827 deletions

View File

@@ -0,0 +1,27 @@
import { InjectionToken } from '@outposts/injection-js';
import type { CheckAuthResultEventType } from 'oidc-client-rx';
import { type Observable, map } from 'rxjs';
import type { AuthMethodType } from './defs';
export abstract class AuthProvider {
abstract authMethod: AuthMethodType;
abstract checkAuthResultEvent$: Observable<CheckAuthResultEventType>;
abstract isAuthenticated$: Observable<boolean>;
abstract userData$: Observable<any>;
abstract getAccessToken(): Observable<string | undefined>;
abstract setup(): void;
abstract autoLoginPartialRoutesGuard(): Observable<boolean>;
getAuthHeaders(): Observable<Record<string, string>> {
return this.getAccessToken().pipe(
map((accessToken) =>
accessToken
? {
Authorization: `Bearer ${accessToken}`,
}
: ({} as Record<string, string>)
)
);
}
}
export const AUTH_PROVIDER = new InjectionToken<AuthProvider>('AUTH_PROVIDER');

View File

@@ -0,0 +1,22 @@
import { UnreachableError } from '@/infra/errors/common';
import type { CheckAuthResultEventType } from 'oidc-client-rx';
import { NEVER, type Observable, of } from 'rxjs';
import { AuthProvider } from '../auth.provider';
import { AUTH_METHOD } from '../defs';
export class BasicAuthProvider extends AuthProvider {
authMethod = AUTH_METHOD.BASIC;
isAuthenticated$ = of(true);
userData$ = of({});
checkAuthResultEvent$: Observable<CheckAuthResultEventType> = NEVER;
getAccessToken(): Observable<string | undefined> {
return of(undefined);
}
setup(): void {}
autoLoginPartialRoutesGuard(): Observable<boolean> {
throw new UnreachableError('Basic auth should always be authenticated');
}
}

View File

@@ -0,0 +1 @@
export { BasicAuthProvider } from './basic-auth.provider';

View File

@@ -0,0 +1,12 @@
import type { ValueOf } from 'type-fest';
export const AUTH_METHOD = {
BASIC: 'basic',
OIDC: 'oidc',
} as const;
export type AuthMethodType = ValueOf<typeof AUTH_METHOD>;
export function getAppAuthMethod(): AuthMethodType {
return process.env.AUTH_TYPE as AuthMethodType;
}

View File

@@ -0,0 +1,35 @@
import { LogLevel, type OpenIdConfiguration } from 'oidc-client-rx';
export function buildOidcConfig(): OpenIdConfiguration {
const origin = window.location.origin;
const resource = process.env.OIDC_AUDIENCE!;
return {
authority: process.env.OIDC_ISSUER!,
redirectUrl: `${origin}/auth/oidc/callback`,
postLogoutRedirectUri: `${origin}/`,
clientId: process.env.OIDC_CLIENT_ID!,
clientSecret: process.env.OIDC_CLIENT_SECRET,
scope: process.env.OIDC_EXTRA_SCOPES
? `openid profile email offline_access ${process.env.OIDC_EXTRA_SCOPES}`
: 'openid profile email offline_access',
triggerAuthorizationResultEvent: true,
responseType: 'code',
silentRenew: true,
useRefreshToken: true,
logLevel: LogLevel.None,
autoUserInfo: !resource,
renewUserInfoAfterTokenRenew: !resource,
customParamsAuthRequest: {
prompt: 'consent',
resource,
},
customParamsRefreshTokenRequest: {
resource,
},
customParamsCodeRequest: {
resource,
},
};
}

View File

@@ -0,0 +1,2 @@
export { buildOidcConfig } from './config';
export { OidcAuthProvider } from './oidc-auth.provider';

View File

@@ -0,0 +1,41 @@
import { injectInjector } from '@/infra/di/inject';
import { inject, runInInjectionContext } from '@outposts/injection-js';
import {
CHECK_AUTH_RESULT_EVENT,
OidcSecurityService,
autoLoginPartialRoutesGuard,
} from 'oidc-client-rx';
import { type Observable, map } from 'rxjs';
import { AuthProvider } from '../auth.provider';
import { AUTH_METHOD } from '../defs';
export class OidcAuthProvider extends AuthProvider {
authMethod = AUTH_METHOD.OIDC;
oidcSecurityService = inject(OidcSecurityService);
checkAuthResultEvent$ = inject(CHECK_AUTH_RESULT_EVENT);
injector = injectInjector();
setup() {
this.oidcSecurityService.checkAuth().subscribe();
}
get isAuthenticated$() {
return this.oidcSecurityService.isAuthenticated$.pipe(
map((s) => s.isAuthenticated)
);
}
get userData$() {
return this.oidcSecurityService.userData$.pipe(map((s) => s.userData));
}
getAccessToken(): Observable<string | undefined> {
return this.oidcSecurityService.getAccessToken();
}
autoLoginPartialRoutesGuard() {
return runInInjectionContext(this.injector, () =>
autoLoginPartialRoutesGuard()
);
}
}

View File

@@ -0,0 +1,16 @@
import type { Injector, Provider } from '@outposts/injection-js';
import { GraphQLService } from './graphql.service';
export function provideGraphql(): Provider[] {
return [GraphQLService];
}
export interface GraphQLContext {
graphqlService: GraphQLService;
}
export function graphqlContextFromInjector(injector: Injector): GraphQLContext {
return {
graphqlService: injector.get(GraphQLService),
};
}

View File

@@ -0,0 +1,30 @@
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { Injectable, inject } from '@outposts/injection-js';
import { firstValueFrom } from 'rxjs';
import { AUTH_PROVIDER } from '../auth/auth.provider.ts';
@Injectable()
export class GraphQLService {
private authProvider = inject(AUTH_PROVIDER);
private apiLink = createHttpLink({
uri: '/api/graphql',
});
private authLink = setContext(async (_, { headers }) => {
const authHeaders = await firstValueFrom(
this.authProvider.getAuthHeaders()
);
return { headers: { ...headers, ...authHeaders } };
});
_apollo = new ApolloClient({
link: this.authLink.concat(this.apiLink),
cache: new InMemoryCache(),
});
query = this._apollo.query;
mutate = this._apollo.mutate;
watchQuery = this._apollo.watchQuery;
}

View File

@@ -0,0 +1,2 @@
export { GraphQLService } from './graphql.service';
export { provideGraphql } from './context';

View File

@@ -1,21 +0,0 @@
import React from 'react';
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined
);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener('change', onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener('change', onChange);
}, []);
return !!isMobile;
}

View File

@@ -1,7 +0,0 @@
import { Injectable, inject } from '@outposts/injection-js';
import { OidcSecurityService } from 'oidc-client-rx';
@Injectable()
export class HttpService {
authService = inject(OidcSecurityService);
}

View File

@@ -1,4 +1,4 @@
import type { ProLinkProps } from '@/components/ui/pro-link';
import type { ProLinkProps } from '@/views/components/ui/pro-link';
import type { Injector } from '@outposts/injection-js';
import type { LucideIcon } from 'lucide-react';

View File

@@ -1,5 +1,5 @@
import { guardRouteIndexAsNotFound } from '@/components/layout/app-not-found';
import type { RouteStateDataOption } from '@/infra/routes/traits';
import { guardRouteIndexAsNotFound } from '@/views/components/layout/app-not-found';
import { Outlet } from '@tanstack/react-router';
export interface BuildVirtualBranchRouteOptions {

View File

@@ -1,7 +0,0 @@
import type { ClassValue } from 'clsx';
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}