refactor: refactor webui structure

This commit is contained in:
2025-04-24 02:23:26 +08:00
parent 68aa13e216
commit eb8f0be004
87 changed files with 407 additions and 385 deletions

View File

@@ -0,0 +1,45 @@
import { LogLevel, type OpenIdConfiguration } from 'oidc-client-rx';
import type { ValueOf } from 'type-fest';
export const AuthMethodEnum = {
BASIC: 'basic',
OIDC: 'oidc',
} as const;
export type AuthMethodType = ValueOf<typeof AuthMethodEnum>;
export const AppAuthMethod = process.env.AUTH_TYPE as AuthMethodType;
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,83 @@
import { UnreachableError } from '@/infra/errors/common';
import type { Injector, Provider } from '@outposts/injection-js';
import type { AnyRouter } from '@tanstack/react-router';
import {
CHECK_AUTH_RESULT_EVENT,
type CheckAuthResultEventType,
OidcSecurityService,
provideAuth as provideOidcAuth,
withCheckAuthResultEvent,
withDefaultFeatures,
} from 'oidc-client-rx';
import { withTanstackRouter } from 'oidc-client-rx/adapters/@tanstack/react-router';
import { NEVER, type Observable, map, of } from 'rxjs';
import { AppAuthMethod, AuthMethodEnum, buildOidcConfig } from './config';
export function provideAuth(router: AnyRouter): Provider[] {
if (AppAuthMethod === AuthMethodEnum.OIDC) {
return provideOidcAuth(
{
config: buildOidcConfig(),
},
withDefaultFeatures({
router: { enabled: false },
securityStorage: { type: 'local-storage' },
}),
withTanstackRouter(router),
withCheckAuthResultEvent()
);
}
return [];
}
export function setupAuthContext(injector: Injector) {
if (AppAuthMethod === AuthMethodEnum.OIDC) {
const oidcSecurityService = injector.get(OidcSecurityService);
oidcSecurityService.checkAuth().subscribe();
}
}
export interface OidcAuthContext {
type: typeof AuthMethodEnum.OIDC;
oidcSecurityService: OidcSecurityService;
isAuthenticated$: Observable<boolean>;
userData$: Observable<{}>;
checkAuthResultEvent$: Observable<CheckAuthResultEventType>;
}
export interface BasicAuthContext {
type: typeof AuthMethodEnum.BASIC;
isAuthenticated$: Observable<true>;
userData$: Observable<{}>;
checkAuthResultEvent$: Observable<CheckAuthResultEventType>;
}
export type AuthContext = OidcAuthContext | BasicAuthContext;
const BASIC_AUTH_IS_AUTHENTICATED$ = of(true) as Observable<true>;
const BASIC_AUTH_USER_DATA$ = of({});
export function authContextFromInjector(injector: Injector): AuthContext {
if (AppAuthMethod === AuthMethodEnum.OIDC) {
const oidcSecurityService = injector.get(OidcSecurityService)!;
return {
type: AuthMethodEnum.OIDC,
oidcSecurityService: injector.get(OidcSecurityService),
isAuthenticated$: oidcSecurityService.isAuthenticated$.pipe(
map((s) => s.isAuthenticated)
),
userData$: oidcSecurityService.userData$.pipe(map((s) => s.userData)),
checkAuthResultEvent$: injector.get(CHECK_AUTH_RESULT_EVENT),
};
}
if (AppAuthMethod === AuthMethodEnum.BASIC) {
return {
type: AuthMethodEnum.BASIC,
isAuthenticated$: BASIC_AUTH_IS_AUTHENTICATED$,
userData$: BASIC_AUTH_USER_DATA$,
checkAuthResultEvent$: NEVER,
};
}
throw new UnreachableError('Invalid auth method');
}

View File

@@ -0,0 +1,21 @@
import type { RouterContext } from '@/infra/routes/traits';
import { runInInjectionContext } from '@outposts/injection-js';
import { autoLoginPartialRoutesGuard } from 'oidc-client-rx';
import { firstValueFrom } from 'rxjs';
import { authContextFromInjector } from './context';
export const beforeLoadGuard = async ({
context,
}: { context: RouterContext }) => {
const { isAuthenticated$ } = authContextFromInjector(context.injector);
if (!(await firstValueFrom(isAuthenticated$))) {
const guard$ = runInInjectionContext(context.injector, () =>
autoLoginPartialRoutesGuard()
);
const isAuthenticated = await firstValueFrom(guard$);
if (!isAuthenticated) {
throw !isAuthenticated;
}
}
};

View File

@@ -0,0 +1,34 @@
import { atomWithObservable } from 'jotai/utils';
import { useInjector } from 'oidc-client-rx/adapters/react';
import { useMemo } from 'react';
import type { Observable } from 'rxjs';
import { authContextFromInjector } from './context';
export function useAuth() {
const injector = useInjector();
const authContext = useMemo(
() => authContextFromInjector(injector),
[injector]
);
const isAuthenticated = useMemo(
() =>
atomWithObservable(
() => authContext.isAuthenticated$ as Observable<boolean>
),
[authContext.isAuthenticated$]
);
const userData = useMemo(
() => atomWithObservable(() => authContext.userData$ as Observable<any>),
[authContext]
);
return {
userData,
injector,
isAuthenticated,
authContext,
};
}