feat: switch to oidc-client-rx
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "recorder"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[lib]
|
||||
@@ -52,7 +52,7 @@ reqwest = { version = "0.12", features = [
|
||||
thiserror = "2"
|
||||
rss = "2"
|
||||
bytes = "1.9"
|
||||
itertools = "0.13.0"
|
||||
itertools = "0.14"
|
||||
url = "2.5"
|
||||
fancy-regex = "0.14"
|
||||
regex = "1.11"
|
||||
@@ -92,7 +92,7 @@ async-graphql = { version = "7.0.13", features = [] }
|
||||
async-graphql-axum = "7.0.13"
|
||||
fastrand = "2.3.0"
|
||||
seaography = "1.1.2"
|
||||
quirks_path = "0.1.0"
|
||||
quirks_path = "0.1.1"
|
||||
base64 = "0.22.1"
|
||||
tower = "0.5.2"
|
||||
axum-extra = "0.10.0"
|
||||
@@ -112,6 +112,7 @@ http-cache = { version = "0.20.0", features = [
|
||||
], default-features = false }
|
||||
http-cache-semantics = "2.1.0"
|
||||
dotenv = "0.15.0"
|
||||
nom = "8.0.0"
|
||||
|
||||
[dev-dependencies]
|
||||
serial_test = "3"
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
{
|
||||
"name": "recorder",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "rsbuild dev",
|
||||
"build": "rsbuild build",
|
||||
"preview": "rsbuild preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@abraham/reflection": "^0.12.0",
|
||||
"@graphiql/react": "^0.28.2",
|
||||
"@graphiql/toolkit": "^0.11.1",
|
||||
"@konobangu/design-system": "workspace:*",
|
||||
"@konobangu/tailwind-config": "workspace:*",
|
||||
"@outposts/injection-js": "^2.5.1",
|
||||
"@tanstack/react-router": "^1.95.6",
|
||||
"@tanstack/router-devtools": "^1.95.6",
|
||||
"graphiql": "^3.8.3",
|
||||
"graphql-ws": "^5.16.2",
|
||||
"oidc-client-ts": "^3.1.0",
|
||||
"observable-hooks": "^4.2.4",
|
||||
"oidc-client-rx": "0.1.0-alpha.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-oidc-context": "^3.2.0"
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@konobangu/typescript-config": "workspace:*",
|
||||
"@rsbuild/core": "^1.1.13",
|
||||
"@rsbuild/plugin-react": "^1.1.0",
|
||||
"@rsbuild/core": "1.1.3",
|
||||
"@rsbuild/plugin-react": "^1.1.1",
|
||||
"@tanstack/router-plugin": "^1.95.6",
|
||||
"@types/react": "^19.0.7",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
|
||||
@@ -2,4 +2,4 @@ export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { RouterProvider, createRouter } from '@tanstack/react-router';
|
||||
import type { UserManager } from 'oidc-client-ts';
|
||||
import { useMemo } from 'react';
|
||||
import { AuthProvider, useAuth } from 'react-oidc-context';
|
||||
import { buildUserManager } from '../auth/config';
|
||||
import { routeTree } from '../routeTree.gen';
|
||||
|
||||
// Set up a Router instance
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
basepath: '/api/playground',
|
||||
defaultPreload: 'intent',
|
||||
context: {
|
||||
isAuthenticated: process.env.AUTH_TYPE === 'basic',
|
||||
auth: undefined!,
|
||||
userManager: undefined!,
|
||||
},
|
||||
});
|
||||
|
||||
// Register things for typesafety
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
}
|
||||
|
||||
const AppWithBasicAuth = () => {
|
||||
return <RouterProvider router={router} />;
|
||||
};
|
||||
|
||||
const AppWithOidcAuthInner = ({
|
||||
userManager,
|
||||
}: { userManager: UserManager }) => {
|
||||
const auth = useAuth();
|
||||
return (
|
||||
<RouterProvider
|
||||
router={router}
|
||||
context={{ isAuthenticated: auth.isAuthenticated, auth, userManager }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const AppWithOidcAuth = () => {
|
||||
const userManager = useMemo(() => buildUserManager(), []);
|
||||
return (
|
||||
<AuthProvider userManager={userManager}>
|
||||
<AppWithOidcAuthInner userManager={userManager} />
|
||||
</AuthProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const App =
|
||||
process.env.AUTH_TYPE === 'oidc' ? AppWithOidcAuth : AppWithBasicAuth;
|
||||
@@ -1,31 +1,42 @@
|
||||
import { type OidcClientSettings, UserManager } from 'oidc-client-ts';
|
||||
import { InjectionToken } from '@outposts/injection-js';
|
||||
import {
|
||||
type EventTypes,
|
||||
LogLevel,
|
||||
type OpenIdConfiguration,
|
||||
} from 'oidc-client-rx';
|
||||
|
||||
export const PostLoginRedirectUriKey = 'post_login_redirect_uri';
|
||||
export const isBasicAuth = process.env.AUTH_TYPE === 'basic';
|
||||
|
||||
export function buildOidcConfig(): OidcClientSettings {
|
||||
export function buildOidcConfig(): OpenIdConfiguration {
|
||||
const origin = window.location.origin;
|
||||
|
||||
const resource = process.env.OIDC_AUDIENCE!;
|
||||
|
||||
return {
|
||||
authority: process.env.OIDC_ISSUER!,
|
||||
client_id: process.env.OIDC_CLIENT_ID!,
|
||||
client_secret: process.env.OIDC_CLIENT_SECRET!,
|
||||
redirect_uri: `${origin}/api/playground/oidc/callback`,
|
||||
disablePKCE: false,
|
||||
scope: `openid profile email ${process.env.OIDC_EXTRA_SCOPES}`,
|
||||
response_type: 'code',
|
||||
resource,
|
||||
post_logout_redirect_uri: `${origin}/api/playground`,
|
||||
extraQueryParams: {
|
||||
redirectUrl: `${origin}/api/playground/oidc/callback`,
|
||||
postLogoutRedirectUri: `${origin}/api/playground`,
|
||||
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.Debug,
|
||||
autoUserInfo: !resource,
|
||||
renewUserInfoAfterTokenRenew: !resource,
|
||||
customParamsAuthRequest: {
|
||||
prompt: 'consent',
|
||||
resource,
|
||||
},
|
||||
extraTokenParams: {
|
||||
customParamsRefreshTokenRequest: {
|
||||
resource,
|
||||
},
|
||||
customParamsCodeRequest: {
|
||||
resource,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function buildUserManager(): UserManager {
|
||||
return new UserManager(buildOidcConfig());
|
||||
}
|
||||
|
||||
41
apps/recorder/src/auth/event.ts
Normal file
41
apps/recorder/src/auth/event.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { Observable } from '@graphiql/toolkit';
|
||||
import { InjectionToken, inject } from '@outposts/injection-js';
|
||||
import {
|
||||
type AuthFeature,
|
||||
EventTypes,
|
||||
PublicEventsService,
|
||||
} from 'oidc-client-rx';
|
||||
import { filter, shareReplay } from 'rxjs';
|
||||
|
||||
export type CheckAuthResultEventType =
|
||||
| { type: EventTypes.CheckingAuthFinished }
|
||||
| {
|
||||
type: EventTypes.CheckingAuthFinishedWithError;
|
||||
value: string;
|
||||
};
|
||||
export const CHECK_AUTH_RESULT_EVENT = new InjectionToken<
|
||||
Observable<CheckAuthResultEventType>
|
||||
>('CHECK_AUTH_RESULT_EVENT');
|
||||
|
||||
export function withCheckAuthResultEvent(): AuthFeature {
|
||||
return {
|
||||
ɵproviders: [
|
||||
{
|
||||
provide: CHECK_AUTH_RESULT_EVENT,
|
||||
useFactory: () => {
|
||||
const publishEventService = inject(PublicEventsService);
|
||||
|
||||
return publishEventService.registerForEvents().pipe(
|
||||
filter(
|
||||
(e) =>
|
||||
e.type === EventTypes.CheckingAuthFinishedWithError ||
|
||||
e.type === EventTypes.CheckingAuthFinished
|
||||
),
|
||||
shareReplay(1)
|
||||
);
|
||||
},
|
||||
deps: [PublicEventsService],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -1,21 +1,19 @@
|
||||
import type { ParsedLocation } from '@tanstack/react-router';
|
||||
import { runInInjectionContext } from '@outposts/injection-js';
|
||||
import { autoLoginPartialRoutesGuard } from 'oidc-client-rx';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import type { RouterContext } from '../controllers/__root';
|
||||
import { PostLoginRedirectUriKey } from './config';
|
||||
|
||||
export const beforeLoadGuard = async ({
|
||||
context,
|
||||
location,
|
||||
// biome-ignore lint/complexity/noBannedTypes: <explanation>
|
||||
}: { context: RouterContext; location: ParsedLocation<{}> }) => {
|
||||
}: { context: RouterContext }) => {
|
||||
if (!context.isAuthenticated) {
|
||||
// TODO: FIXME
|
||||
const user = await context.userManager.getUser();
|
||||
if (!user) {
|
||||
try {
|
||||
sessionStorage.setItem(PostLoginRedirectUriKey, location.href);
|
||||
// biome-ignore lint/suspicious/noEmptyBlockStatements: <explanation>
|
||||
} catch {}
|
||||
throw await context.auth.signinRedirect();
|
||||
const guard$ = runInInjectionContext(context.injector, () =>
|
||||
autoLoginPartialRoutesGuard()
|
||||
);
|
||||
|
||||
const isAuthenticated = await firstValueFrom(guard$);
|
||||
if (!isAuthenticated) {
|
||||
throw !isAuthenticated;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
52
apps/recorder/src/auth/hooks.ts
Normal file
52
apps/recorder/src/auth/hooks.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useObservableEagerState, useObservableState } from 'observable-hooks';
|
||||
import {
|
||||
InjectorContextVoidInjector,
|
||||
useOidcClient,
|
||||
} from 'oidc-client-rx/adapters/react';
|
||||
import { useMemo } from 'react';
|
||||
import { NEVER, type Observable, of } from 'rxjs';
|
||||
import { isBasicAuth } from './config';
|
||||
import {
|
||||
CHECK_AUTH_RESULT_EVENT,
|
||||
type CheckAuthResultEventType,
|
||||
} from './event';
|
||||
|
||||
const BASIC_AUTH_IS_AUTHENTICATED$ = of({
|
||||
isAuthenticated: true,
|
||||
allConfigsAuthenticated: [],
|
||||
});
|
||||
|
||||
const BASIC_AUTH_USER_DATA$ = of({
|
||||
userData: {},
|
||||
allUserData: [],
|
||||
});
|
||||
|
||||
export function useAuth() {
|
||||
const { oidcSecurityService, injector } = isBasicAuth
|
||||
? { oidcSecurityService: undefined, injector: InjectorContextVoidInjector }
|
||||
: // biome-ignore lint/correctness/useHookAtTopLevel: <explanation>
|
||||
useOidcClient();
|
||||
|
||||
const { isAuthenticated } = useObservableEagerState(
|
||||
oidcSecurityService?.isAuthenticated$ ?? BASIC_AUTH_IS_AUTHENTICATED$
|
||||
);
|
||||
|
||||
const { userData } = useObservableEagerState(
|
||||
oidcSecurityService?.userData$ ?? BASIC_AUTH_USER_DATA$
|
||||
);
|
||||
|
||||
const checkAuthResultEvent = useObservableState(
|
||||
useMemo(
|
||||
() => (isBasicAuth ? NEVER : injector.get(CHECK_AUTH_RESULT_EVENT)),
|
||||
[injector]
|
||||
) as Observable<CheckAuthResultEventType>
|
||||
);
|
||||
|
||||
return {
|
||||
oidcSecurityService,
|
||||
isAuthenticated,
|
||||
userData,
|
||||
injector,
|
||||
checkAuthResultEvent,
|
||||
};
|
||||
}
|
||||
@@ -1,22 +1,22 @@
|
||||
import type { Injector } from '@outposts/injection-js';
|
||||
import {
|
||||
Link,
|
||||
// Link,
|
||||
Outlet,
|
||||
createRootRouteWithContext,
|
||||
} from '@tanstack/react-router';
|
||||
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
|
||||
import type { UserManager } from 'oidc-client-ts';
|
||||
import type { AuthContextProps } from 'react-oidc-context';
|
||||
import type { OidcSecurityService } from 'oidc-client-rx';
|
||||
|
||||
export type RouterContext =
|
||||
| {
|
||||
isAuthenticated: false;
|
||||
auth: AuthContextProps;
|
||||
userManager: UserManager;
|
||||
injector: Injector;
|
||||
oidcSecurityService: OidcSecurityService;
|
||||
}
|
||||
| {
|
||||
isAuthenticated: true;
|
||||
auth?: AuthContextProps;
|
||||
userManager?: UserManager;
|
||||
injector?: Injector;
|
||||
oidcSecurityService?: OidcSecurityService;
|
||||
};
|
||||
|
||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { createGraphiQLFetcher } from '@graphiql/toolkit';
|
||||
import { type Fetcher, createGraphiQLFetcher } from '@graphiql/toolkit';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import GraphiQL from 'graphiql';
|
||||
import { useMemo } from 'react';
|
||||
import { useAuth } from 'react-oidc-context';
|
||||
import { beforeLoadGuard } from '../../auth/guard';
|
||||
import 'graphiql/graphiql.css';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { useAuth } from '../../auth/hooks';
|
||||
|
||||
export const Route = createFileRoute('/graphql/')({
|
||||
component: RouteComponent,
|
||||
@@ -12,19 +13,23 @@ export const Route = createFileRoute('/graphql/')({
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const auth = useAuth();
|
||||
const { oidcSecurityService } = useAuth();
|
||||
|
||||
const fetcher = useMemo(
|
||||
() =>
|
||||
createGraphiQLFetcher({
|
||||
(): Fetcher => async (props) => {
|
||||
const accessToken = oidcSecurityService
|
||||
? await firstValueFrom(oidcSecurityService.getAccessToken())
|
||||
: undefined;
|
||||
return createGraphiQLFetcher({
|
||||
url: '/api/graphql',
|
||||
headers: auth?.user?.access_token
|
||||
headers: accessToken
|
||||
? {
|
||||
Authorization: `Bearer ${auth.user.access_token}`,
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
}
|
||||
: undefined,
|
||||
}),
|
||||
[auth]
|
||||
})(props);
|
||||
},
|
||||
[oidcSecurityService]
|
||||
);
|
||||
|
||||
return <GraphiQL fetcher={fetcher} className="h-svh" />;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router';
|
||||
import { useEffect } from 'react';
|
||||
import { useAuth } from 'react-oidc-context';
|
||||
import { PostLoginRedirectUriKey } from '../../auth/config';
|
||||
import { EventTypes } from 'oidc-client-rx';
|
||||
import { useAuth } from '../../auth/hooks';
|
||||
|
||||
export const Route = createFileRoute('/oidc/callback')({
|
||||
component: RouteComponent,
|
||||
beforeLoad: ({ context }) => {
|
||||
if (!context.auth) {
|
||||
if (!context.oidcSecurityService) {
|
||||
throw redirect({
|
||||
to: '/',
|
||||
});
|
||||
@@ -17,26 +16,17 @@ export const Route = createFileRoute('/oidc/callback')({
|
||||
function RouteComponent() {
|
||||
const auth = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
if (!auth?.isLoading && auth?.isAuthenticated) {
|
||||
try {
|
||||
const redirectUri = sessionStorage.getItem(PostLoginRedirectUriKey);
|
||||
if (redirectUri) {
|
||||
history.replaceState(null, '', redirectUri);
|
||||
}
|
||||
// biome-ignore lint/suspicious/noEmptyBlockStatements: <explanation>
|
||||
} catch {}
|
||||
}
|
||||
}, [auth]);
|
||||
|
||||
if (auth?.isLoading) {
|
||||
if (!auth.checkAuthResultEvent) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
OpenID Connect Auth Callback Result:{' '}
|
||||
{auth.error ? auth.error?.message : 'unknown'}
|
||||
OpenID Connect Auth Callback:{' '}
|
||||
{auth.checkAuthResultEvent?.type ===
|
||||
EventTypes.CheckingAuthFinishedWithError
|
||||
? auth.checkAuthResultEvent.value
|
||||
: 'success'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* @TODO: rewrite with nom
|
||||
*/
|
||||
use std::borrow::Cow;
|
||||
|
||||
use itertools::Itertools;
|
||||
@@ -322,7 +325,7 @@ pub fn parse_episode_meta_from_raw_name(s: &str) -> color_eyre::eyre::Result<Raw
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::{parse_episode_meta_from_raw_name, RawEpisodeMeta};
|
||||
use super::{RawEpisodeMeta, parse_episode_meta_from_raw_name};
|
||||
|
||||
fn test_raw_ep_parser_case(raw_name: &str, expected: &str) {
|
||||
let expected: Option<RawEpisodeMeta> = serde_json::from_str(expected).unwrap_or_default();
|
||||
|
||||
@@ -1,15 +1,96 @@
|
||||
import '@abraham/reflection';
|
||||
import { type Injector, ReflectiveInjector } from '@outposts/injection-js';
|
||||
import { RouterProvider, createRouter } from '@tanstack/react-router';
|
||||
import {
|
||||
OidcSecurityService,
|
||||
provideAuth,
|
||||
withDefaultFeatures,
|
||||
} from 'oidc-client-rx';
|
||||
import {
|
||||
InjectorContextVoidInjector,
|
||||
InjectorProvider,
|
||||
} from 'oidc-client-rx/adapters/react';
|
||||
import { withTanstackRouter } from 'oidc-client-rx/adapters/tanstack-router';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { App } from './app';
|
||||
import { buildOidcConfig, isBasicAuth } from './auth/config';
|
||||
import { withCheckAuthResultEvent } from './auth/event';
|
||||
import { useAuth } from './auth/hooks';
|
||||
import { routeTree } from './routeTree.gen';
|
||||
import './main.css';
|
||||
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
basepath: '/api/playground',
|
||||
defaultPreload: 'intent',
|
||||
context: {
|
||||
isAuthenticated: isBasicAuth,
|
||||
injector: InjectorContextVoidInjector,
|
||||
oidcSecurityService: {} as OidcSecurityService,
|
||||
},
|
||||
});
|
||||
|
||||
// Register things for typesafety
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
}
|
||||
|
||||
const injector: Injector = isBasicAuth
|
||||
? ReflectiveInjector.resolveAndCreate([])
|
||||
: ReflectiveInjector.resolveAndCreate(
|
||||
provideAuth(
|
||||
{
|
||||
config: buildOidcConfig(),
|
||||
},
|
||||
withDefaultFeatures({
|
||||
router: { enabled: false },
|
||||
securityStorage: { type: 'local-storage' },
|
||||
}),
|
||||
withTanstackRouter(router),
|
||||
withCheckAuthResultEvent()
|
||||
)
|
||||
);
|
||||
|
||||
// if needed, check when init
|
||||
let oidcSecurityService: OidcSecurityService | undefined;
|
||||
if (!isBasicAuth) {
|
||||
oidcSecurityService = injector.get(OidcSecurityService);
|
||||
oidcSecurityService.checkAuth().subscribe();
|
||||
}
|
||||
|
||||
const AppWithBasicAuth = () => {
|
||||
return <RouterProvider router={router} />;
|
||||
};
|
||||
|
||||
const AppWithOidcAuth = () => {
|
||||
const { isAuthenticated, oidcSecurityService, injector } = useAuth();
|
||||
return (
|
||||
<RouterProvider
|
||||
router={router}
|
||||
context={{
|
||||
isAuthenticated,
|
||||
oidcSecurityService,
|
||||
injector,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const App = isBasicAuth ? AppWithBasicAuth : AppWithOidcAuth;
|
||||
|
||||
const rootEl = document.getElementById('root');
|
||||
|
||||
if (rootEl) {
|
||||
rootEl.classList.add('min-h-svh');
|
||||
const root = ReactDOM.createRoot(rootEl);
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<InjectorProvider injector={injector}>
|
||||
<App />
|
||||
</InjectorProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true
|
||||
|
||||
Reference in New Issue
Block a user