Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/aform 3712 form authentication js sdk #47

Merged
merged 3 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion samples/ManagementSite/Alloy.ManagementSite.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@
<PackageReference Include="EPiServer.CMS.UI.VisitorGroups" Version="12.22.7" />
<PackageReference Include="EPiServer.CMS.UI.AspNetIdentity" Version="12.22.7" />
<PackageReference Include="EPiServer.ImageLibrary.ImageSharp" Version="2.0.1" />
<PackageReference Include="Optimizely.Headless.Form.Service" Version="0.1.0--inte-148" />
<PackageReference Include="Optimizely.Headless.Form.Service" Version="0.1.0--inte-159" />
<PackageReference Include="Optimizely.Cms.Content.EPiServer" Version="0.3.1" />
<PackageReference Include="EPiServer.Forms" Version="5.6.1-feature-AFORM-3540-021035" />
<PackageReference Include="EPiServer.OpenIDConnect" Version="3.2.0"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="EPiServer.ContentDefinitionsApi" Version="3.3.0" />
Expand Down
57 changes: 57 additions & 0 deletions samples/ManagementSite/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
using Microsoft.Extensions.Hosting;
using Optimizely.Headless.Form.DependencyInjection;
using Optimizely.Headless.Form;
using EPiServer.OpenIDConnect;
using System;
using static OpenIddict.Abstractions.OpenIddictConstants;
using Microsoft.Extensions.Options;
using OpenIddict.Server;
using System.Linq;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Alloy.ManagementSite
{
Expand All @@ -24,6 +31,9 @@ public class Startup
private readonly IWebHostEnvironment _environment;
private readonly IConfiguration _configuration;
private readonly string _allowedOrigins = "_allowedOrigins";
private const string TestClientId = "TestClient";
private const string TestClientSecret = "TestClientSecret";
private const string ClientEndpoint = "http://localhost:8082";

public Startup(IWebHostEnvironment environment, IConfiguration configuration)
{
Expand Down Expand Up @@ -91,6 +101,28 @@ public void ConfigureServices(IServiceCollection services)
});
});

services.AddOpenIDConnect<ApplicationUser>(
useDevelopmentCertificate: true,
signingCertificate: null,
encryptionCertificate: null,
createSchema: true,
options =>
{
options.AllowResourceOwnerPasswordFlow = true;
options.AccessTokenLifetime = TimeSpan.FromHours(24);
options.RequireHttps = false;
options.Applications.Add(new OpenIDConnectApplication
{
ClientId = TestClientId,
Scopes =
{
Scopes.OpenId,
},
});
});

services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<HeadlessFormServiceOptions>, HeadlessFormServiceOptionsPostConfigure>());

// Register the Optimizely Headless Form API Services
services.AddOptimizelyHeadlessFormService(options =>
{
Expand All @@ -100,6 +132,10 @@ public void ConfigureServices(IServiceCollection services)
AllowOrigins = new string[] { "http://localhost:3000" }, //Enter '*' to allow any origins, multiple origins separate by comma
AllowCredentials = true
};
options.OpenIDConnectClients.Add(new()
{
Authority = ClientEndpoint
});
});
}

Expand All @@ -122,4 +158,25 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
});
}
}

public class HeadlessFormServiceOptionsPostConfigure : IPostConfigureOptions<HeadlessFormServiceOptions>
{
private readonly OpenIddictServerOptions _options;

public HeadlessFormServiceOptionsPostConfigure(IOptions<OpenIddictServerOptions> options)
{
_options = options.Value;
}

public void PostConfigure(string name, HeadlessFormServiceOptions options)
{
foreach (var client in options.OpenIDConnectClients)
{
foreach (var key in _options.EncryptionCredentials.Select(c => c.Key))
{
client.EncryptionKeys.Add(key);
}
}
}
}
}
3 changes: 2 additions & 1 deletion samples/sample-react-app/.env
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
REACT_APP_ENDPOINT_GET_FORM_BY_PAGE_URL=http://localhost:8082/api/React/GetFormInPageByUrl?url=
REACT_APP_HEADLESS_FORM_BASE_URL=http://localhost:8082/
REACT_APP_HEADLESS_FORM_BASE_URL=http://localhost:8082/
REACT_APP_AUTH_BASEURL=http://localhost:8082/api/episerver/connect/token
22 changes: 20 additions & 2 deletions samples/sample-react-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,43 @@
import './App.css';
import { useFetch } from './hooks/useFetch';
import { Form } from '@episerver/forms-react';
import { Form, FormLogin } from '@episerver/forms-react';
import { extractParams } from './helpers/urlHelper';
import { FormCache, FormConstants, IdentityInfo } from '@episerver/forms-sdk';
import { useState } from 'react';

function App() {
const { relativePath, language } = extractParams(window.location.pathname)
const url = `${process.env.REACT_APP_ENDPOINT_GET_FORM_BY_PAGE_URL}${relativePath}`;
const {data: pageData, loading} = useFetch(url);
const formCache = new FormCache();
const [identityInfo, setIdentityInfo] = useState<IdentityInfo>({
accessToken: formCache.get<string>(FormConstants.FormAccessToken)
} as IdentityInfo);

const handleAuthen = (identityInfo: IdentityInfo) => {
setIdentityInfo(identityInfo);
}

return (
<div className="App">
{loading && <div className='loading'>Loading...</div>}

{!loading && pageData && (
<>
<h1>{pageData.title}</h1>
<h2>Login</h2>
<FormLogin
clientId='TestClient'
authBaseUrl={process.env.REACT_APP_AUTH_BASEURL ?? ""}
onAuthenticated={handleAuthen} />

{pageData.childrens.map((c: any) => (
<Form
key={c.reference.key}
formKey={c.reference.key}
language={language ?? "en"}
baseUrl={process.env.REACT_APP_HEADLESS_FORM_BASE_URL}/>
baseUrl={process.env.REACT_APP_HEADLESS_FORM_BASE_URL}
identityInfo={identityInfo}/>
))}
</>
)}
Expand Down
11 changes: 8 additions & 3 deletions src/@episerver/forms-react/src/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";
import { FormContainerBlock } from "./components/FormContainerBlock";
import { UseFormLoaderProps, useFormLoader } from "./hooks/useFormLoader";
import "./Form.scss"
import { IdentityInfo } from "@episerver/forms-sdk";

interface FormProps {
/**
Expand All @@ -15,15 +16,19 @@ interface FormProps {
/**
* The base url of Headless Form API
*/
baseUrl?: string
baseUrl?: string,
/**
* Access token for form submit
*/
identityInfo?: IdentityInfo
}

export const Form = ({formKey, language, baseUrl}: FormProps) => {
export const Form = ({formKey, language, baseUrl, identityInfo}: FormProps) => {
const {data: formData } = useFormLoader({ formKey, language, baseUrl } as UseFormLoaderProps)

return (
<>
{formData && <FormContainerBlock form={formData} key={formData.key} />}
{formData && <FormContainerBlock form={formData} key={formData.key} identityInfo={identityInfo} />}
</>
);
}
65 changes: 65 additions & 0 deletions src/@episerver/forms-react/src/FormLogin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { FormAuthenticate, FormAuthenticateConfig, FormCache, FormConstants, IdentityInfo, isNullOrEmpty } from "@episerver/forms-sdk";
import React, { useState } from "react";

interface FormLoginProps{
/**
* Client Id that's allowed access API
*/
clientId: string,
/**
* Endpoint to get access token
*/
authBaseUrl: string,
/**
* Callback function when authenticated
* @param identityInfo
* @returns
*/
onAuthenticated?: (identityInfo: IdentityInfo) => void
}

export const FormLogin = (props: FormLoginProps) => {
const [loginInfo, setLoginInfo] = useState<any>({
username: "",
password: ""
});
const formAuthenticate = new FormAuthenticate({
clientId: props.clientId,
grantType: "password",
authBaseUrl: props.authBaseUrl
} as FormAuthenticateConfig);
const formCache = new FormCache();
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(!isNullOrEmpty(formCache.get<string>(FormConstants.FormAccessToken)));

const handleChange = (e: any) => {
setLoginInfo({...loginInfo, [e.target.name]: e.target.value});
}

const handleClick = () => {
formAuthenticate.login(loginInfo.username, loginInfo.password).then((token) => {
setIsAuthenticated(true);
formCache.set<string>(FormConstants.FormAccessToken, token);
props.onAuthenticated && props.onAuthenticated({ username: loginInfo.username, accessToken: token } as IdentityInfo);
});
}

return (
<>
{isAuthenticated ? (<div className="Form__Authenticated">Authenticated</div>) : (
<div className="Form__Login">
<div className="Form__Login__Username">
<label htmlFor="username">Username:</label>
<input id="username" name="username" type="text" onChange={handleChange} value={loginInfo.username} />
</div>
<div className="Form__Login__Password">
<label htmlFor="password">Password:</label>
<input id="password" name="password" type="password" onChange={handleChange} value={loginInfo.password} />
</div>
<div>
<button className="Form__Login__Submit" onClick={handleClick}>Login</button>
</div>
</div>
)}
</>
);
}
23 changes: 19 additions & 4 deletions src/@episerver/forms-react/src/components/FormBody.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import React, { useRef } from "react";
import React, { useEffect, useRef } from "react";
import { useForms, useFormsDispatch } from "../context/store";
import { FormContainer, FormSubmit, SubmitButtonType, equals, isInArray, isNull, isNullOrEmpty } from "@episerver/forms-sdk";
import { FormContainer, FormSubmit, IdentityInfo, SubmitButtonType, equals, isInArray, isNull, isNullOrEmpty } from "@episerver/forms-sdk";
import { RenderElementInStep } from "./RenderElementInStep";
import { DispatchFunctions } from "../context/dispatchFunctions";

export const FormBody = () => {
interface FormBodyProps {
identityInfo?: IdentityInfo
}

export const FormBody = (props: FormBodyProps) => {
const formContext = useForms();
const form = formContext?.formContainer ?? {} as FormContainer;
const formSubmit = new FormSubmit(formContext?.formContainer ?? {} as FormContainer);
const dispatch = useFormsDispatch();
const dispatchFunctions = new DispatchFunctions(dispatch);

const formTitleId = `${form.key}_label`;
const statusDisplay = useRef<string>("hide");
const stepCount = form.steps.length;
const statusMessage = useRef<string>("");
const statusDisplay = useRef<string>("hide");
const stepLocalizations = useRef<Record<string, string>>(form.steps?.filter(s => !isNull(s.formStep.localizations))[0]?.formStep.localizations);

//TODO: these variables should be get from api or sdk
Expand Down Expand Up @@ -71,6 +75,17 @@ export const FormBody = () => {
formSubmit.doSubmit(formSubmissions);
}

useEffect(()=>{
dispatchFunctions.dispatchUpdateIdentity(props.identityInfo);
if(isNullOrEmpty(props.identityInfo?.accessToken) && !form.properties.allowAnonymousSubmission){
statusDisplay.current = "Form__Warning__Message";
statusMessage.current = "You must be logged in to submit this form. If you are logged in and still cannot post, make sure \"Do not track\" in your browser settings is disabled.";
}
else {
statusDisplay.current = "hide";
}
},[props.identityInfo?.accessToken])

return (
<form method="post"
noValidate={true}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from "react";
import { FormContainer, StepBuilder, initFormState } from "@episerver/forms-sdk";
import { FormContainer, IdentityInfo, StepBuilder, initFormState } from "@episerver/forms-sdk";
import { FormProvider } from "../context/FormProvider";
import { FormBody } from "./FormBody";

export interface FormContainerProps {
form: FormContainer
identityInfo?: IdentityInfo
}

export function FormContainerBlock(props: FormContainerProps){
Expand All @@ -15,7 +16,7 @@ export function FormContainerBlock(props: FormContainerProps){
{/* finally return the form */}
return (
<FormProvider initialState={state}>
<FormBody />
<FormBody identityInfo={props.identityInfo}/>
</FormProvider>
)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FormContainer, ElementValidationResult, FormValidationResult, initFormState } from "@episerver/forms-sdk";
import { FormContainer, ElementValidationResult, FormValidationResult, initFormState, IdentityInfo } from "@episerver/forms-sdk";
import { ActionType } from "./reducer";

export class DispatchFunctions {
Expand Down Expand Up @@ -59,4 +59,11 @@ export class DispatchFunctions {
focusOn
});
}

dispatchUpdateIdentity = (identityInfo?: IdentityInfo) => {
this._dispatch({
type: ActionType.UpdateIdentityInfo,
identityInfo
});
}
}
14 changes: 12 additions & 2 deletions src/@episerver/forms-react/src/context/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export enum ActionType {
ResetForm = "ResetForm",
ResetedForm = "ResetedForm",
UpdateAllValidation = "UpdateAllValidation",
UpdateFocusOn = "UpdateFocusOn"
UpdateFocusOn = "UpdateFocusOn",
UpdateIdentityInfo = "UpdateIdentityInfo"
}

export function formReducer(formState: FormState, action: any) {
Expand Down Expand Up @@ -46,7 +47,10 @@ export function formReducer(formState: FormState, action: any) {
} as FormState;
}
case ActionType.ResetForm: {
return action.formState;
return {
...formState,
...action.formState
};
}
case ActionType.ResetedForm: {
return {
Expand All @@ -60,6 +64,12 @@ export function formReducer(formState: FormState, action: any) {
focusOn: action.focusOn
} as FormState;
}
case ActionType.UpdateIdentityInfo: {
return {
...formState,
identityInfo: action.identityInfo
} as FormState;
}
default: {
return formState;
}
Expand Down
3 changes: 2 additions & 1 deletion src/@episerver/forms-react/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./Form";
export * from "./Form";
export * from "./FormLogin";
Loading