mirror of
https://codeberg.org/MarkusThielker/next-ory.git
synced 2025-07-01 20:49:18 +00:00
Initial commit
This commit is contained in:
commit
a74e7f3ebd
84 changed files with 11089 additions and 0 deletions
199
authentication/src/ory/hooks.tsx
Normal file
199
authentication/src/ory/hooks.tsx
Normal file
|
@ -0,0 +1,199 @@
|
|||
'use client';
|
||||
|
||||
import { AxiosError } from 'axios';
|
||||
import React, { DependencyList, useEffect, useState } from 'react';
|
||||
|
||||
import { kratos } from './sdk/kratos';
|
||||
import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime';
|
||||
|
||||
export const HandleError = (
|
||||
getFlow:
|
||||
| ((flowId: string) => Promise<void | AxiosError>)
|
||||
| undefined = undefined,
|
||||
setFlow: React.Dispatch<React.SetStateAction<any>> | undefined = undefined,
|
||||
defaultNav: string | undefined = undefined,
|
||||
fatalToError = false,
|
||||
router: AppRouterInstance,
|
||||
) => {
|
||||
return async (
|
||||
error: AxiosError<any, unknown>,
|
||||
): Promise<AxiosError | void> => {
|
||||
if (!error.response || error.response?.status === 0) {
|
||||
window.location.href = `/flow/error?error=${encodeURIComponent(
|
||||
JSON.stringify(error.response),
|
||||
)}`;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const responseData = error.response?.data || {};
|
||||
|
||||
switch (error.response?.status) {
|
||||
case 400: {
|
||||
if (responseData.error?.id == 'session_already_available') {
|
||||
router.push('/');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// the request could contain invalid parameters which would set error messages in the flow
|
||||
if (setFlow !== undefined) {
|
||||
console.warn('sdkError 400: update flow data');
|
||||
setFlow(responseData);
|
||||
return Promise.resolve();
|
||||
}
|
||||
break;
|
||||
}
|
||||
// we have no session or the session is invalid
|
||||
case 401: {
|
||||
console.warn('handleError hook 401: Navigate to /login');
|
||||
router.push('/flow/login');
|
||||
return Promise.resolve();
|
||||
}
|
||||
case 403: {
|
||||
// the user might have a session, but would require 2FA (Two-Factor Authentication)
|
||||
if (responseData.error?.id === 'session_aal2_required') {
|
||||
router.push('/flow/login?aal2=true');
|
||||
router.refresh();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (
|
||||
responseData.error?.id === 'session_refresh_required' &&
|
||||
responseData.redirect_browser_to
|
||||
) {
|
||||
console.warn(
|
||||
'sdkError 403: Redirect browser to',
|
||||
responseData.redirect_browser_to,
|
||||
);
|
||||
window.location = responseData.redirect_browser_to;
|
||||
return Promise.resolve();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 404: {
|
||||
console.warn('sdkError 404: Navigate to Error');
|
||||
const errorMsg = {
|
||||
data: error.response?.data || error,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: window.location.href,
|
||||
};
|
||||
|
||||
router.push(
|
||||
`/flow/error?error=${encodeURIComponent(JSON.stringify(errorMsg))}`,
|
||||
);
|
||||
return Promise.resolve();
|
||||
}
|
||||
// error.id handling
|
||||
// "self_service_flow_expired"
|
||||
case 410: {
|
||||
if (getFlow !== undefined && responseData.use_flow_id !== undefined) {
|
||||
console.warn('sdkError 410: Update flow');
|
||||
return getFlow(responseData.use_flow_id).catch((error) => {
|
||||
// Something went seriously wrong - log and redirect to defaultNav if possible
|
||||
console.error(error);
|
||||
|
||||
if (defaultNav !== undefined) {
|
||||
router.push(defaultNav);
|
||||
} else {
|
||||
// Rethrow error when can't navigate and let caller handle
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
} else if (defaultNav !== undefined) {
|
||||
console.warn('sdkError 410: Navigate to', defaultNav);
|
||||
router.push(defaultNav);
|
||||
return Promise.resolve();
|
||||
}
|
||||
break;
|
||||
}
|
||||
// we need to parse the response and follow the `redirect_browser_to` URL
|
||||
// this could be when the user needs to perform a 2FA challenge
|
||||
// or passwordless login
|
||||
case 422: {
|
||||
if (responseData.redirect_browser_to !== undefined) {
|
||||
const currentUrl = new URL(window.location.href);
|
||||
const redirect = new URL(responseData.redirect_browser_to);
|
||||
|
||||
// host name has changed, then change location
|
||||
if (currentUrl.host !== redirect.host) {
|
||||
console.warn('sdkError 422: Host changed redirect');
|
||||
window.location = responseData.redirect_browser_to;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Path has changed
|
||||
if (currentUrl.pathname !== redirect.pathname) {
|
||||
console.warn('sdkError 422: Update path');
|
||||
router.push(redirect.pathname + redirect.search);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// for webauthn we need to reload the flow
|
||||
const flowId = redirect.searchParams.get('flow');
|
||||
|
||||
if (flowId != null && getFlow !== undefined) {
|
||||
// get new flow data based on the flow id in the redirect url
|
||||
console.warn('sdkError 422: Update flow');
|
||||
return getFlow(flowId).catch((error) => {
|
||||
// Something went seriously wrong - log and redirect to defaultNav if possible
|
||||
console.error(error);
|
||||
|
||||
if (defaultNav !== undefined) {
|
||||
router.push(defaultNav);
|
||||
} else {
|
||||
// Rethrow error when can't navigate and let caller handle
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn('sdkError 422: Redirect browser to');
|
||||
window.location = responseData.redirect_browser_to;
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
|
||||
if (fatalToError) {
|
||||
console.warn('sdkError: fatal error redirect to /error');
|
||||
router.push('/flow/error?id=' + encodeURI(error.response?.data.error?.id));
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
throw error;
|
||||
};
|
||||
};
|
||||
|
||||
// Returns a function which will log the user out
|
||||
export function LogoutLink(deps?: DependencyList) {
|
||||
|
||||
const [logoutToken, setLogoutToken] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
kratos
|
||||
.createBrowserLogoutFlow()
|
||||
.then(({ data }) => {
|
||||
setLogoutToken(data.logout_token);
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
switch (err.response?.status) {
|
||||
case 401:
|
||||
// do nothing, the user is not logged in
|
||||
return;
|
||||
}
|
||||
|
||||
// Something else happened!
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}, deps);
|
||||
|
||||
return () => {
|
||||
if (logoutToken) {
|
||||
kratos
|
||||
.updateLogoutFlow({ token: logoutToken })
|
||||
.then(() => window.location.href = '/flow/login');
|
||||
}
|
||||
};
|
||||
}
|
3
authentication/src/ory/index.ts
Normal file
3
authentication/src/ory/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './hooks';
|
||||
export * from './ui';
|
||||
export * from './sdk/kratos';
|
15
authentication/src/ory/sdk/hydra/index.ts
Normal file
15
authentication/src/ory/sdk/hydra/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
'use server';
|
||||
|
||||
import { Configuration, OAuth2Api } from '@ory/client';
|
||||
|
||||
// implemented as a function because of 'use server'
|
||||
export default async function getHydra() {
|
||||
return new OAuth2Api(new Configuration(
|
||||
new Configuration({
|
||||
basePath: process.env.ORY_HYDRA_ADMIN_URL,
|
||||
baseOptions: {
|
||||
withCredentials: true,
|
||||
},
|
||||
}),
|
||||
));
|
||||
}
|
14
authentication/src/ory/sdk/kratos/index.ts
Normal file
14
authentication/src/ory/sdk/kratos/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
'use client';
|
||||
|
||||
import { Configuration, FrontendApi } from '@ory/client';
|
||||
|
||||
const kratos = new FrontendApi(
|
||||
new Configuration({
|
||||
basePath: process.env.NEXT_PUBLIC_ORY_KRATOS_URL,
|
||||
baseOptions: {
|
||||
withCredentials: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export { kratos };
|
224
authentication/src/ory/ui/Flow.tsx
Normal file
224
authentication/src/ory/ui/Flow.tsx
Normal file
|
@ -0,0 +1,224 @@
|
|||
'use client';
|
||||
|
||||
import {
|
||||
LoginFlow,
|
||||
RecoveryFlow,
|
||||
RegistrationFlow,
|
||||
SettingsFlow,
|
||||
UiNode,
|
||||
UpdateLoginFlowBody,
|
||||
UpdateRecoveryFlowBody,
|
||||
UpdateRegistrationFlowBody,
|
||||
UpdateSettingsFlowBody,
|
||||
UpdateVerificationFlowBody,
|
||||
VerificationFlow,
|
||||
} from '@ory/client';
|
||||
import { getNodeId, isUiNodeInputAttributes } from '@ory/integrations/ui';
|
||||
import { Component, FormEvent, MouseEvent } from 'react';
|
||||
|
||||
import { Messages, Node } from '@/ory';
|
||||
|
||||
export type Values = Partial<
|
||||
| UpdateLoginFlowBody
|
||||
| UpdateRegistrationFlowBody
|
||||
| UpdateRecoveryFlowBody
|
||||
| UpdateSettingsFlowBody
|
||||
| UpdateVerificationFlowBody
|
||||
>
|
||||
|
||||
export type Methods =
|
||||
| 'oidc'
|
||||
| 'password'
|
||||
| 'profile'
|
||||
| 'totp'
|
||||
| 'webauthn'
|
||||
| 'passkey'
|
||||
| 'link'
|
||||
| 'lookup_secret'
|
||||
|
||||
export type Props<T> = {
|
||||
// The flow
|
||||
flow?:
|
||||
| LoginFlow
|
||||
| RegistrationFlow
|
||||
| SettingsFlow
|
||||
| VerificationFlow
|
||||
| RecoveryFlow
|
||||
// Only show certain nodes. We will always render the default nodes for CSRF tokens.
|
||||
only?: Methods
|
||||
// Is triggered on submission
|
||||
onSubmit: (values: T) => Promise<void>
|
||||
// Do not show the global messages. Useful when rendering them elsewhere.
|
||||
hideGlobalMessages?: boolean
|
||||
}
|
||||
|
||||
function emptyState<T>() {
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
type State<T> = {
|
||||
values: T
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
export class Flow<T extends Values> extends Component<Props<T>, State<T>> {
|
||||
constructor(props: Props<T>) {
|
||||
super(props);
|
||||
this.state = {
|
||||
values: emptyState(),
|
||||
isLoading: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.initializeValues(this.filterNodes());
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props<T>) {
|
||||
if (prevProps.flow !== this.props.flow) {
|
||||
// Flow has changed, reload the values!
|
||||
this.initializeValues(this.filterNodes());
|
||||
}
|
||||
}
|
||||
|
||||
initializeValues = (nodes: Array<UiNode> = []) => {
|
||||
// Compute the values
|
||||
const values = emptyState<T>();
|
||||
nodes.forEach((node) => {
|
||||
// This only makes sense for text nodes
|
||||
if (isUiNodeInputAttributes(node.attributes)) {
|
||||
if (
|
||||
node.attributes.type === 'button' ||
|
||||
node.attributes.type === 'submit'
|
||||
) {
|
||||
// In order to mimic real HTML forms, we need to skip setting the value
|
||||
// for buttons as the button value will (in normal HTML forms) only trigger
|
||||
// if the user clicks it.
|
||||
return;
|
||||
}
|
||||
values[node.attributes.name as keyof Values] = node.attributes.value;
|
||||
}
|
||||
});
|
||||
|
||||
// Set all the values!
|
||||
this.setState((state) => ({ ...state, values }));
|
||||
};
|
||||
|
||||
filterNodes = (): Array<UiNode> => {
|
||||
const { flow, only } = this.props;
|
||||
if (!flow) {
|
||||
return [];
|
||||
}
|
||||
return flow.ui.nodes.filter(({ group }) => {
|
||||
if (!only) {
|
||||
return true;
|
||||
}
|
||||
return group === 'default' || group === only;
|
||||
});
|
||||
};
|
||||
|
||||
// Handles form submission
|
||||
handleSubmit = (event: FormEvent<HTMLFormElement> | MouseEvent) => {
|
||||
// Prevent all native handlers
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
// Prevent double submission!
|
||||
if (this.state.isLoading) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const form = event.currentTarget;
|
||||
|
||||
let body: T | undefined;
|
||||
|
||||
if (form && form instanceof HTMLFormElement) {
|
||||
const formData = new FormData(form);
|
||||
|
||||
// map the entire form data to JSON for the request body
|
||||
body = Object.fromEntries(formData) as T;
|
||||
|
||||
const hasSubmitter = (evt: any): evt is { submitter: HTMLInputElement } =>
|
||||
'submitter' in evt;
|
||||
|
||||
// We need the method specified from the name and value of the submit button.
|
||||
// when multiple submit buttons are present, the clicked one's value is used.
|
||||
if (hasSubmitter(event.nativeEvent)) {
|
||||
const method = event.nativeEvent.submitter;
|
||||
body = {
|
||||
...body,
|
||||
...{ [method.name]: method.value },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
this.setState((state) => ({
|
||||
...state,
|
||||
isLoading: true,
|
||||
}));
|
||||
|
||||
return this.props
|
||||
.onSubmit({ ...body, ...this.state.values })
|
||||
.finally(() => {
|
||||
// We wait for reconciliation and update the state after 50ms
|
||||
// Done submitting - update loading status
|
||||
this.setState((state) => ({
|
||||
...state,
|
||||
isLoading: false,
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { hideGlobalMessages, flow } = this.props;
|
||||
const { values, isLoading } = this.state;
|
||||
|
||||
// Filter the nodes - only show the ones we want
|
||||
const nodes = this.filterNodes();
|
||||
|
||||
if (!flow) {
|
||||
// No flow was set yet? It's probably still loading...
|
||||
//
|
||||
// Nodes have only one element? It is probably just the CSRF Token
|
||||
// and the filter did not match any elements!
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
action={flow.ui.action}
|
||||
method={flow.ui.method}
|
||||
onSubmit={this.handleSubmit}
|
||||
className="flex flex-col w-full space-y-2"
|
||||
>
|
||||
{!hideGlobalMessages ? <Messages classNames="space-y-2" messages={flow.ui.messages}/> : null}
|
||||
{nodes.map((node, k) => {
|
||||
const id = getNodeId(node) as keyof Values;
|
||||
return (
|
||||
<Node
|
||||
key={`${id}-${k}`}
|
||||
disabled={isLoading}
|
||||
node={node}
|
||||
value={values[id]}
|
||||
dispatchSubmit={this.handleSubmit}
|
||||
setValue={(value) =>
|
||||
new Promise((resolve) => {
|
||||
this.setState(
|
||||
(state) => ({
|
||||
...state,
|
||||
values: {
|
||||
...state.values,
|
||||
[getNodeId(node)]: value,
|
||||
},
|
||||
}),
|
||||
resolve,
|
||||
);
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
54
authentication/src/ory/ui/Messages.tsx
Normal file
54
authentication/src/ory/ui/Messages.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { UiText } from '@ory/client';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { JSX } from 'react';
|
||||
import { AlertCircle, AlertOctagon, Check } from 'lucide-react';
|
||||
|
||||
interface MessageProps {
|
||||
message: UiText,
|
||||
}
|
||||
|
||||
export const Message = ({ message }: MessageProps) => {
|
||||
|
||||
let icon: JSX.Element = <></>;
|
||||
switch (message.type) {
|
||||
case 'error':
|
||||
icon = <AlertOctagon className="h-4 w-4"/>;
|
||||
break;
|
||||
case 'success':
|
||||
icon = <Check className="h-4 w-4"/>;
|
||||
break;
|
||||
case 'info':
|
||||
icon = <AlertCircle className="h-4 w-4"/>;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert>
|
||||
{icon}
|
||||
<AlertTitle>{message.type.charAt(0).toUpperCase() + message.type.substring(1)}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{message.text}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
interface MessagesProps {
|
||||
messages?: Array<UiText>
|
||||
classNames?: string,
|
||||
}
|
||||
|
||||
export const Messages = ({ messages, classNames }: MessagesProps) => {
|
||||
if (!messages) {
|
||||
// No messages? Do nothing.
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames}>
|
||||
{messages.map((message) => (
|
||||
<Message key={message.id} message={message}/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
60
authentication/src/ory/ui/Node.tsx
Normal file
60
authentication/src/ory/ui/Node.tsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
import { UiNode } from '@ory/client';
|
||||
import {
|
||||
isUiNodeAnchorAttributes,
|
||||
isUiNodeImageAttributes,
|
||||
isUiNodeInputAttributes,
|
||||
isUiNodeScriptAttributes,
|
||||
isUiNodeTextAttributes,
|
||||
} from '@ory/integrations/ui';
|
||||
|
||||
import { NodeAnchor } from './NodeAnchor';
|
||||
import { NodeImage, NodeInput, NodeText } from '@/ory';
|
||||
import { NodeScript } from './NodeScript';
|
||||
import { FormDispatcher, ValueSetter } from './helpers';
|
||||
|
||||
interface Props {
|
||||
node: UiNode;
|
||||
disabled: boolean;
|
||||
value: any;
|
||||
setValue: ValueSetter;
|
||||
dispatchSubmit: FormDispatcher;
|
||||
}
|
||||
|
||||
export const Node = ({
|
||||
node,
|
||||
value,
|
||||
setValue,
|
||||
disabled,
|
||||
dispatchSubmit,
|
||||
}: Props) => {
|
||||
if (isUiNodeImageAttributes(node.attributes)) {
|
||||
return <NodeImage node={node} attributes={node.attributes}/>;
|
||||
}
|
||||
|
||||
if (isUiNodeScriptAttributes(node.attributes)) {
|
||||
return <NodeScript node={node} attributes={node.attributes}/>;
|
||||
}
|
||||
|
||||
if (isUiNodeTextAttributes(node.attributes)) {
|
||||
return <NodeText node={node} attributes={node.attributes}/>;
|
||||
}
|
||||
|
||||
if (isUiNodeAnchorAttributes(node.attributes)) {
|
||||
return <NodeAnchor node={node} attributes={node.attributes}/>;
|
||||
}
|
||||
|
||||
if (isUiNodeInputAttributes(node.attributes)) {
|
||||
return (
|
||||
<NodeInput
|
||||
dispatchSubmit={dispatchSubmit}
|
||||
value={value}
|
||||
setValue={setValue}
|
||||
node={node}
|
||||
disabled={disabled}
|
||||
attributes={node.attributes}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
23
authentication/src/ory/ui/NodeAnchor.tsx
Normal file
23
authentication/src/ory/ui/NodeAnchor.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
'use client';
|
||||
|
||||
import { UiNode, UiNodeAnchorAttributes } from '@ory/client';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface Props {
|
||||
node: UiNode;
|
||||
attributes: UiNodeAnchorAttributes;
|
||||
}
|
||||
|
||||
export const NodeAnchor = ({ node, attributes }: Props) => {
|
||||
return (
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
window.location.href = attributes.href;
|
||||
}}
|
||||
>
|
||||
{attributes.title.text}
|
||||
</Button>
|
||||
);
|
||||
};
|
16
authentication/src/ory/ui/NodeImage.tsx
Normal file
16
authentication/src/ory/ui/NodeImage.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { UiNode, UiNodeImageAttributes } from '@ory/client';
|
||||
|
||||
interface Props {
|
||||
node: UiNode;
|
||||
attributes: UiNodeImageAttributes;
|
||||
}
|
||||
|
||||
export const NodeImage = ({ node, attributes }: Props) => {
|
||||
return (
|
||||
<img
|
||||
src={attributes.src}
|
||||
width={200}
|
||||
alt={node.meta.label?.text}
|
||||
/>
|
||||
);
|
||||
};
|
29
authentication/src/ory/ui/NodeInput.tsx
Normal file
29
authentication/src/ory/ui/NodeInput.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { NodeInputButton } from './NodeInputButton';
|
||||
import { NodeInputCheckbox } from './NodeInputCheckbox';
|
||||
import { NodeInputDefault } from './NodeInputDefault';
|
||||
import { NodeInputHidden } from './NodeInputHidden';
|
||||
import { NodeInputSubmit } from './NodeInputSubmit';
|
||||
import { NodeInputProps } from './helpers';
|
||||
|
||||
export function NodeInput<T>(props: NodeInputProps) {
|
||||
const { attributes } = props;
|
||||
|
||||
switch (attributes.type) {
|
||||
case 'hidden':
|
||||
// Render a hidden input field
|
||||
return <NodeInputHidden {...props} />;
|
||||
case 'checkbox':
|
||||
// Render a checkbox. We have one hidden element which is the real value (true/false), and one
|
||||
// display element which is the toggle value (true)!
|
||||
return <NodeInputCheckbox {...props} />;
|
||||
case 'button':
|
||||
// Render a button
|
||||
return <NodeInputButton {...props} />;
|
||||
case 'submit':
|
||||
// Render the submit button
|
||||
return <NodeInputSubmit {...props} />;
|
||||
}
|
||||
|
||||
// Render a generic text input field.
|
||||
return <NodeInputDefault {...props} />;
|
||||
}
|
47
authentication/src/ory/ui/NodeInputButton.tsx
Normal file
47
authentication/src/ory/ui/NodeInputButton.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
'use client';
|
||||
|
||||
import { getNodeLabel } from '@ory/integrations/ui';
|
||||
|
||||
import { callWebauthnFunction, NodeInputProps } from './helpers';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import React from 'react';
|
||||
|
||||
export function NodeInputButton<T>({
|
||||
node,
|
||||
attributes,
|
||||
setValue,
|
||||
disabled,
|
||||
dispatchSubmit,
|
||||
}: NodeInputProps) {
|
||||
// Some attributes have dynamic JavaScript - this is for example required for WebAuthn.
|
||||
const onClick = (e: React.MouseEvent | React.FormEvent<HTMLFormElement>) => {
|
||||
// This section is only used for WebAuthn. The script is loaded via a <script> node
|
||||
// and the functions are available on the global window level. Unfortunately, there
|
||||
// is currently no better way than executing eval / function here at this moment.
|
||||
//
|
||||
// Please note that we also need to prevent the default action from happening.
|
||||
if (attributes.onclick) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
callWebauthnFunction(attributes.onclick);
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(attributes.value).then(() => dispatchSubmit(e));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
name={attributes.name}
|
||||
onClick={(e) => {
|
||||
onClick(e);
|
||||
}}
|
||||
value={attributes.value || ''}
|
||||
disabled={attributes.disabled || disabled}
|
||||
>
|
||||
{getNodeLabel(node)}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
36
authentication/src/ory/ui/NodeInputCheckbox.tsx
Normal file
36
authentication/src/ory/ui/NodeInputCheckbox.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
'use client';
|
||||
|
||||
import { NodeInputProps } from './helpers';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
|
||||
export function NodeInputCheckbox<T>(
|
||||
{
|
||||
node,
|
||||
attributes,
|
||||
setValue,
|
||||
disabled,
|
||||
}: NodeInputProps,
|
||||
) {
|
||||
|
||||
const state =
|
||||
node.messages.find(({ type }) => type === 'error') ? 'error' : undefined;
|
||||
|
||||
// Render a checkbox.
|
||||
return (
|
||||
<div className={`inline-flex space-x-2 items-center ${state ? 'text-yellow-500' : undefined}`}>
|
||||
<Checkbox
|
||||
id={attributes.name}
|
||||
name={attributes.name}
|
||||
defaultChecked={attributes.value}
|
||||
onCheckedChange={(e) => setValue(e)}
|
||||
disabled={attributes.disabled || disabled}
|
||||
className={`my-2 ${state ? 'border-yellow-500' : undefined}`}
|
||||
/>
|
||||
<label
|
||||
htmlFor={attributes.name}
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
dangerouslySetInnerHTML={{ __html: node.meta.label?.text || '' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
48
authentication/src/ory/ui/NodeInputDefault.tsx
Normal file
48
authentication/src/ory/ui/NodeInputDefault.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
'use client';
|
||||
|
||||
import { NodeInputProps } from './helpers';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
|
||||
export function NodeInputDefault<T>(props: NodeInputProps) {
|
||||
const { node, attributes, value = '', setValue, disabled } = props;
|
||||
|
||||
// Some attributes have dynamic JavaScript - this is for example required for WebAuthn.
|
||||
const onClick = () => {
|
||||
// This section is only used for WebAuthn. The script is loaded via a <script> node
|
||||
// and the functions are available on the global window level. Unfortunately, there
|
||||
// is currently no better way than executing eval / function here at this moment.
|
||||
if (attributes.onclick) {
|
||||
const run = new Function(attributes.onclick);
|
||||
run();
|
||||
}
|
||||
};
|
||||
|
||||
const state =
|
||||
node.messages.find(({ type }) => type === 'error') ? 'error' : undefined;
|
||||
|
||||
// Render a generic text input field.
|
||||
return (
|
||||
<div>
|
||||
<Label className={state ? 'text-yellow-500' : undefined}>{node.meta.label?.text}</Label>
|
||||
<Input
|
||||
title={node.meta.label?.text}
|
||||
onClick={onClick}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
placeholder={node.meta.label?.text}
|
||||
type={attributes.type}
|
||||
name={attributes.name}
|
||||
value={value}
|
||||
autoComplete={attributes.autocomplete}
|
||||
disabled={attributes.disabled || disabled}
|
||||
/>
|
||||
{node.messages.map(({ text, id }, k) => (
|
||||
<Label className="text-yellow-500 inline-flex space-x-2 items-center mt-1.5" key={`${id}-${k}`}>
|
||||
<AlertTriangle className="h-4 w-4"/>
|
||||
<span>{text}</span>
|
||||
</Label>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
15
authentication/src/ory/ui/NodeInputHidden.tsx
Normal file
15
authentication/src/ory/ui/NodeInputHidden.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
'use client';
|
||||
|
||||
import { NodeInputProps, useOnload } from './helpers';
|
||||
|
||||
export function NodeInputHidden<T>({ attributes }: NodeInputProps) {
|
||||
useOnload(attributes as any);
|
||||
|
||||
return (
|
||||
<input
|
||||
type={attributes.type}
|
||||
name={attributes.name}
|
||||
value={attributes.value || 'true'}
|
||||
/>
|
||||
);
|
||||
}
|
24
authentication/src/ory/ui/NodeInputSubmit.tsx
Normal file
24
authentication/src/ory/ui/NodeInputSubmit.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
'use client';
|
||||
|
||||
import { getNodeLabel } from '@ory/integrations/ui';
|
||||
|
||||
import { NodeInputProps } from './helpers';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export function NodeInputSubmit<T>(
|
||||
{
|
||||
node,
|
||||
attributes,
|
||||
disabled,
|
||||
}: NodeInputProps,
|
||||
) {
|
||||
return (
|
||||
<Button
|
||||
name={attributes.name}
|
||||
value={attributes.value || ''}
|
||||
disabled={attributes.disabled || disabled}
|
||||
>
|
||||
{getNodeLabel(node)}
|
||||
</Button>
|
||||
);
|
||||
}
|
32
authentication/src/ory/ui/NodeScript.tsx
Normal file
32
authentication/src/ory/ui/NodeScript.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
'use client';
|
||||
|
||||
import { UiNode, UiNodeScriptAttributes } from '@ory/client';
|
||||
import { HTMLAttributeReferrerPolicy, useEffect } from 'react';
|
||||
|
||||
interface Props {
|
||||
node: UiNode;
|
||||
attributes: UiNodeScriptAttributes;
|
||||
}
|
||||
|
||||
export const NodeScript = ({ attributes }: Props) => {
|
||||
useEffect(() => {
|
||||
const script = document.createElement('script');
|
||||
|
||||
script.async = true;
|
||||
script.src = attributes.src;
|
||||
script.async = attributes.async;
|
||||
script.crossOrigin = attributes.crossorigin;
|
||||
script.integrity = attributes.integrity;
|
||||
script.referrerPolicy =
|
||||
attributes.referrerpolicy as HTMLAttributeReferrerPolicy;
|
||||
script.type = attributes.type;
|
||||
|
||||
document.body.appendChild(script);
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(script);
|
||||
};
|
||||
}, [attributes]);
|
||||
|
||||
return null;
|
||||
};
|
42
authentication/src/ory/ui/NodeText.tsx
Normal file
42
authentication/src/ory/ui/NodeText.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { UiNode, UiNodeTextAttributes, UiText } from '@ory/client';
|
||||
|
||||
interface Props {
|
||||
node: UiNode;
|
||||
attributes: UiNodeTextAttributes;
|
||||
}
|
||||
|
||||
const Content = ({ node, attributes }: Props) => {
|
||||
switch (attributes.text.id) {
|
||||
case 1050015:
|
||||
// This text node contains lookup secrets. Let's make them a bit more beautiful!
|
||||
const secrets = (attributes.text.context as any).secrets.map(
|
||||
(text: UiText, k: number) => (
|
||||
<div key={k} className="text-sm">
|
||||
<code>{text.id === 1050014 ? 'Used' : text.text}</code>
|
||||
</div>
|
||||
),
|
||||
);
|
||||
return (
|
||||
<div className="container-fluid">
|
||||
<div className="row">{secrets}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full p-4 rounded-md border flex-wrap text-sm">
|
||||
{attributes.text.text}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const NodeText = ({ node, attributes }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<p className="text-lg">
|
||||
{node.meta?.label?.text}
|
||||
</p>
|
||||
<Content node={node} attributes={attributes}/>
|
||||
</>
|
||||
);
|
||||
};
|
44
authentication/src/ory/ui/helpers.ts
Normal file
44
authentication/src/ory/ui/helpers.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { UiNode, UiNodeInputAttributes } from '@ory/client';
|
||||
import { FormEvent, MouseEvent, useEffect } from 'react';
|
||||
|
||||
export type ValueSetter = (
|
||||
value: string | number | boolean | undefined,
|
||||
) => Promise<void>
|
||||
|
||||
export type FormDispatcher = (
|
||||
e: FormEvent<HTMLFormElement> | MouseEvent,
|
||||
) => Promise<void>
|
||||
|
||||
export interface NodeInputProps {
|
||||
node: UiNode;
|
||||
attributes: UiNodeInputAttributes;
|
||||
value: any;
|
||||
disabled: boolean;
|
||||
dispatchSubmit: FormDispatcher;
|
||||
setValue: ValueSetter;
|
||||
}
|
||||
|
||||
export const useOnload = (attributes: { onload?: string }) => {
|
||||
useEffect(() => {
|
||||
if (attributes.onload) {
|
||||
const intervalHandle = callWebauthnFunction(attributes.onload);
|
||||
|
||||
return () => {
|
||||
window.clearInterval(intervalHandle);
|
||||
};
|
||||
}
|
||||
}, [attributes]);
|
||||
};
|
||||
|
||||
export const callWebauthnFunction = (functionBody: string) => {
|
||||
const run = new Function(functionBody);
|
||||
|
||||
const intervalHandle = window.setInterval(() => {
|
||||
if ((window as any).__oryWebAuthnInitialized) {
|
||||
run();
|
||||
window.clearInterval(intervalHandle);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return intervalHandle;
|
||||
};
|
6
authentication/src/ory/ui/index.tsx
Normal file
6
authentication/src/ory/ui/index.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
export * from './Flow';
|
||||
export * from './Messages';
|
||||
export * from './Node';
|
||||
export * from './NodeImage';
|
||||
export * from './NodeInput';
|
||||
export * from './NodeText';
|
Loading…
Add table
Add a link
Reference in a new issue