Skip to content

Commit 936ce25

Browse files
Kratos SSO (#13)
* registration flow updaters * google example, remove some options * prevent setting update and some flows from displaying * reroute after register * simplify node preperations * format
1 parent 43ad497 commit 936ce25

File tree

6 files changed

+173
-67
lines changed

6 files changed

+173
-67
lines changed

pages/login.tsx

Lines changed: 69 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ import ory from "@/pkg/sdk"
1515
import { MiscInfo } from "@/services/basic-fetch/misc"
1616

1717
const Login: NextPage = () => {
18-
const [initialFlow, setInitialFlow] = useState<LoginFlow>()
19-
const [changedFlow, setChangedFlow] = useState<LoginFlow>()
18+
const [initialFlow, setInitialFlow] = useState<LoginFlow>();
19+
const [changedFlow, setChangedFlow] = useState<LoginFlow>();
20+
const [oidcFlow, setOidcFlow] = useState<LoginFlow>();
2021
const [selectedRole, setSelectedRole] = useState<string | undefined>('engineer');
22+
const [isAccLinkageRequested, setIsAccLinkageRequested] = useState(false);
2123

2224
// Get ?flow=... from the URL
2325
const router = useRouter()
@@ -62,20 +64,41 @@ const Login: NextPage = () => {
6264

6365
useEffect(() => {
6466
if (!initialFlow) return;
65-
const data: any = { ...initialFlow };
66-
if (data.ui.nodes[1].meta.label) {
67-
data.ui.nodes[1].meta.label.text = "Email address"
68-
if (MiscInfo.isDemo) {
69-
data.ui.nodes[1].attributes.value = getValueIdentifier(selectedRole);
70-
}
67+
const linkageRequested = initialFlow.ui.messages?.some((message: any) => message.id === 4000007) && initialFlow.ui.messages?.some((message: any) => message.id === 1010016);
68+
if (linkageRequested) {
69+
initialFlow.ui.nodes = []
70+
initialFlow.ui.messages = [{ id: 400002, type: "error", text: "An account with the same identifier exists already and can not be linked by social-sign-in." }]
71+
setChangedFlow(initialFlow);
72+
setIsAccLinkageRequested(linkageRequested);
73+
return
74+
}
75+
76+
const flowData: any = Object.assign({}, initialFlow);
77+
78+
let emailNode = flowData.ui.nodes.find((node: any) => node.meta?.label?.text == "E-Mail");
79+
if (emailNode && MiscInfo.isDemo) {
80+
emailNode.attributes.value = getValueIdentifier(selectedRole);
7181
}
72-
if (data.ui.nodes[2].meta.label && MiscInfo.isDemo) {
73-
data.ui.nodes[2].attributes.value = getValuePassword(selectedRole);
82+
83+
let passwordNode = flowData.ui.nodes.find((node: any) => node.meta?.label?.text == "Password");
84+
if (passwordNode && MiscInfo.isDemo) {
85+
passwordNode.attributes.value = getValuePassword(selectedRole);
7486
}
75-
if (data.ui.nodes[3].meta.label && MiscInfo.isDemo) {
76-
data.ui.nodes[3].meta.label.text = "Proceed"
87+
88+
let submitNode = flowData.ui.nodes.find((node: any) => node.meta?.label?.text == "Sign in");
89+
if (submitNode && MiscInfo.isDemo) {
90+
submitNode.meta.label.text = "Proceed"
7791
}
78-
setChangedFlow(data);
92+
93+
94+
if (flowData.ui.nodes.some((node: any) => node.group === "oidc")) {
95+
const oidcData = JSON.parse(JSON.stringify(flowData));
96+
oidcData.ui.nodes = oidcData.ui.nodes.filter((node: any) => node.group == "oidc");
97+
// prevent duplicate messages
98+
oidcData.ui.messages = [];
99+
setOidcFlow(oidcData);
100+
}
101+
setChangedFlow(flowData);
79102
}, [initialFlow, selectedRole])
80103

81104
const onSubmit = (values: UpdateLoginFlowBody) =>
@@ -133,29 +156,41 @@ const Login: NextPage = () => {
133156
</>)}</>
134157
) : (<></>)}
135158
<div className="ui-container">
136-
{!MiscInfo.isDemo ? (<Flow onSubmit={onSubmit} flow={changedFlow} />) : (<>
137-
<fieldset>
138-
<span className="typography-h3">
139-
Select role
140-
<span className="required-indicator">*</span>
141-
<select className="typography-h3 select" id="roles" value={selectedRole} onChange={(e: any) => { setSelectedRole(e.target.value); }}>
142-
<option value="engineer">Engineer</option>
143-
<option value="expert">Expert</option>
144-
<option value="annotator">Annotator</option>
145-
</select>
146-
</span>
147-
</fieldset>
148-
<p className="text-description" id="description">
149-
{selectedRole === 'engineer' ? 'Administers the project and works on programmatic tasks such as labeling automation or filter settings.' : selectedRole === 'expert' ? 'Working on reference manual labels, which can be used by the engineering team to estimate the data quality.' : 'Working on manual labels as if they were heuristics. They can be switched on/off by the engineering team, so that the engineers can in - or exclude them during weak supervision.'}
150-
</p>
151-
<p className="text-description" id="sub-description">
152-
{selectedRole === 'engineer' ? 'They have access to all features of the application, including the Python SDK.' : selectedRole === 'expert' ? 'They have access to the labeling view only.' : 'They have access to a task-minimized labeling view only. Engineers can revoke their access to the labeling view.'}
153-
</p>
154-
<DemoFlow onSubmit={onSubmit} flow={changedFlow} />
155-
</>)}
159+
{!MiscInfo.isDemo ? (
160+
<div>
161+
<Flow onSubmit={onSubmit} flow={changedFlow} only="password" />
162+
{oidcFlow ?
163+
<>
164+
<div className="divider-outer"><span className="divider">Or</span></div>
165+
<Flow onSubmit={onSubmit} flow={oidcFlow} only="oidc" />
166+
</> : null}
167+
</div>) : (<>
168+
<fieldset>
169+
<span className="typography-h3">
170+
Select role
171+
<span className="required-indicator">*</span>
172+
<select className="typography-h3 select" id="roles" value={selectedRole} onChange={(e: any) => { setSelectedRole(e.target.value); }}>
173+
<option value="engineer">Engineer</option>
174+
<option value="expert">Expert</option>
175+
<option value="annotator">Annotator</option>
176+
</select>
177+
</span>
178+
</fieldset>
179+
<p className="text-description" id="description">
180+
{selectedRole === 'engineer' ? 'Administers the project and works on programmatic tasks such as labeling automation or filter settings.' : selectedRole === 'expert' ? 'Working on reference manual labels, which can be used by the engineering team to estimate the data quality.' : 'Working on manual labels as if they were heuristics. They can be switched on/off by the engineering team, so that the engineers can in - or exclude them during weak supervision.'}
181+
</p>
182+
<p className="text-description" id="sub-description">
183+
{selectedRole === 'engineer' ? 'They have access to all features of the application, including the Python SDK.' : selectedRole === 'expert' ? 'They have access to the labeling view only.' : 'They have access to a task-minimized labeling view only. Engineers can revoke their access to the labeling view.'}
184+
</p>
185+
<DemoFlow onSubmit={onSubmit} flow={changedFlow} />
186+
</>)}
156187
</div>
157188
<div className="link-container">
158-
{!MiscInfo.isDemo ? (<a className="link" data-testid="forgot-password" href="/auth/recovery">Forgot your password?</a>) : (<></>)}
189+
{!MiscInfo.isDemo ?
190+
!isAccLinkageRequested ?
191+
<a className="link" data-testid="forgot-password" href="/auth/recovery">Forgot your password?</a>
192+
: <a className="link" data-testid="back-to-login" href="/auth/login">Go back to login</a>
193+
: null}
159194
</div>
160195
</div>
161196
</div >

pages/registration.tsx

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,18 @@ import ory from "@/pkg/sdk"
99
import { handleFlowError } from "@/pkg/errors"
1010
import { Flow } from "@/pkg"
1111
import { MiscInfo } from "@/services/basic-fetch/misc"
12-
import { prepareFirstLastNameAsRequired } from "@/util/helper-functions"
12+
import { prepareNodes } from "@/util/helper-functions"
1313

1414
// Renders the registration page
1515
const Registration: NextPage = () => {
1616
const router = useRouter()
1717

1818
// The "flow" represents a registration process and contains
1919
// information about the form we need to render (e.g. username + password)
20-
const [initialFlow, setInitialFlow]: any = useState<RegistrationFlow>()
21-
const [changedFlow, setChangedFlow]: any = useState<RegistrationFlow>()
20+
const [initialFlow, setInitialFlow]: any = useState<RegistrationFlow>();
21+
const [changedFlow, setChangedFlow]: any = useState<RegistrationFlow>();
22+
const [oidcFlow, setOidcFlow]: any = useState<RegistrationFlow>();
23+
2224

2325
// Get ?flow=... from the URL
2426
const { flow: flowId, return_to: returnTo } = router.query;
@@ -55,21 +57,32 @@ const Registration: NextPage = () => {
5557

5658
useEffect(() => {
5759
if (!initialFlow) return;
58-
if (initialFlow.ui.nodes[1].meta.label) {
59-
initialFlow.ui.nodes[1].meta.label.text = "Email address"
60+
61+
initialFlow.ui.nodes = prepareNodes(initialFlow);
62+
63+
if (initialFlow.ui.nodes.some((node: any) => node.group === "oidc")) {
64+
const oidcData = JSON.parse(JSON.stringify(initialFlow));
65+
oidcData.ui.nodes = oidcData.ui.nodes.filter((node: any) => node.group == "oidc");
66+
// prevent duplicate messages
67+
oidcData.ui.messages = [];
68+
setOidcFlow(oidcData);
6069
}
61-
initialFlow.ui.nodes = prepareFirstLastNameAsRequired(3, 4, initialFlow);
70+
6271
setChangedFlow(initialFlow);
6372
}, [initialFlow])
6473

6574
const onSubmit = async (values: UpdateRegistrationFlowBody) => {
75+
76+
await router
77+
// On submission, add the flow ID to the URL but do not navigate. This prevents the user loosing
78+
// his data when she/he reloads the page.
79+
.push(`/registration?flow=${initialFlow?.id}`, undefined, { shallow: true })
6680
ory
6781
.updateRegistrationFlow({
6882
flow: String(initialFlow?.id),
69-
updateRegistrationFlowBody: values,
83+
updateRegistrationFlowBody: values
7084
})
7185
.then(async ({ data }) => {
72-
// If continue_with did not contain anything, we can just return to the home page.
7386
await router.push(initialFlow?.return_to || "/")
7487
})
7588
.catch(handleFlowError(router, "registration", setInitialFlow))
@@ -95,7 +108,15 @@ const Registration: NextPage = () => {
95108
<KernLogo />
96109
<div id="signup">
97110
<h2 className="title">{MiscInfo.isManaged ? 'Register account' : 'Sign up for a local account'}</h2>
98-
<Flow onSubmit={onSubmit} flow={changedFlow} />
111+
<div>
112+
<Flow onSubmit={onSubmit} flow={changedFlow} only="password" />
113+
{oidcFlow ?
114+
<>
115+
<div className="divider-outer"><span className="divider">Or</span></div>
116+
<Flow onSubmit={onSubmit} flow={oidcFlow} only="oidc" />
117+
</> : null}
118+
</div>
119+
99120
<div className="link-container">
100121
<a className="link" data-testid="forgot-password" href="/auth/login">Go back to login</a>
101122
</div>

pages/settings.tsx

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ import { Flow, Messages } from "../pkg"
99
import { handleFlowError } from "../pkg/errors"
1010
import ory from "../pkg/sdk"
1111
import { KernLogo } from "@/pkg/ui/Icons"
12-
import { prepareFirstLastNameAsRequired } from "@/util/helper-functions"
12+
import { prepareNodes } from "@/util/helper-functions"
1313

1414
const Settings: NextPage = () => {
1515
const [initialFlow, setInitialFlow]: any = useState<SettingsFlow>()
1616
const [changedFlow, setChangedFlow]: any = useState<SettingsFlow>()
1717
const [containsTotp, setContainsTotp] = useState<boolean>(false)
1818
const [containsBackupCodes, setContainsBackupCodes] = useState<boolean>(false)
19-
19+
const [isOidc, setIsOidc] = useState(false);
2020
// Get ?flow=... from the URL
2121
const router = useRouter()
2222
const { flow: flowId, return_to: returnTo } = router.query
@@ -51,10 +51,7 @@ const Settings: NextPage = () => {
5151

5252
useEffect(() => {
5353
if (!initialFlow) return;
54-
if (initialFlow.ui.nodes[1].meta.label) {
55-
initialFlow.ui.nodes[1].meta.label.text = "Email address";
56-
}
57-
initialFlow.ui.nodes = prepareFirstLastNameAsRequired(2, 3, initialFlow);
54+
initialFlow.ui.nodes = prepareNodes(initialFlow);
5855
const checkIfTotp = initialFlow.ui.nodes.find((node: UiNode) => node.group === "totp");
5956
const checkIfBackupCodes = initialFlow.ui.nodes.find((node: UiNode) => node.group === "lookup_secret");
6057
if (checkIfTotp) {
@@ -64,6 +61,13 @@ const Settings: NextPage = () => {
6461
setContainsBackupCodes(true);
6562
}
6663
setChangedFlow(initialFlow)
64+
65+
//prevent password change option display if sso
66+
if (initialFlow.identity.metadata_public?.registration_scope?.provider_id != "kern.ai") {
67+
initialFlow.ui.nodes = initialFlow.ui.nodes.filter((node: UiNode) => node.group !== "password");
68+
setIsOidc(true);
69+
}
70+
6771
}, [initialFlow])
6872

6973
const onSubmit = (values: UpdateSettingsFlowBody) =>
@@ -109,17 +113,17 @@ const Settings: NextPage = () => {
109113
flow={changedFlow}
110114
/>
111115
</div>
112-
113-
<div className="form-container">
114-
<h3 className="subtitle">Change password</h3>
115-
<Messages messages={changedFlow?.ui.messages} />
116-
<Flow
117-
hideGlobalMessages
118-
onSubmit={onSubmit}
119-
only="password"
120-
flow={changedFlow}
121-
/>
122-
</div>
116+
{!isOidc ?
117+
<div className="form-container">
118+
<h3 className="subtitle">Change password</h3>
119+
<Messages messages={changedFlow?.ui.messages} />
120+
<Flow
121+
hideGlobalMessages
122+
onSubmit={onSubmit}
123+
only="password"
124+
flow={changedFlow}
125+
/>
126+
</div> : null}
123127

124128
{containsBackupCodes ? (<div className="form-container">
125129
<h3 className="subtitle">Manage 2FA backup recovery codes</h3>

pkg/ui/Messages.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ interface MessageProps {
88
export const Message = ({ message }: MessageProps) => {
99
return (
1010
<Alert severity={message.type === "error" ? "error" : "info"}>
11-
<AlertContent data-testid={`ui/message/${message.id}`} className={message.type == 'info' ? 'message success' : 'message error'}>
11+
<AlertContent data-testid={`ui/message/${message.id}`} className={message.type == 'error' ? 'message error' : 'message success'}>
1212
{message.text}
1313
</AlertContent>
1414
</Alert>

styles/globals.css

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,18 @@ button[name="method"] {
8080
background-color: #2f7241 !important;
8181
}
8282

83+
button[name="provider"] {
84+
background-color: white !important;
85+
color: rgb(69, 69, 69) !important;
86+
border: 1px rgb(209 213 219) solid !important;
87+
}
88+
89+
button[name="provider"]:hover {
90+
border-color: #5AB370 !important;
91+
background-color: rgb(251, 251, 251) !important;
92+
transition: all 0.1s ease-in-out;
93+
}
94+
8395
button.button-sign-in:focus {
8496
outline-offset: 2px;
8597
outline-color: #5AB370;
@@ -146,6 +158,27 @@ button.button-sign-in:focus {
146158
margin-bottom: 20px;
147159
}
148160

161+
.divider-outer {
162+
display: flex;
163+
align-items: center;
164+
justify-content: center;
165+
margin: 0.75rem;
166+
padding-top: 0.25rem;
167+
}
168+
169+
.divider-outer:before, .divider-outer:after {
170+
content: "";
171+
border-bottom: 1px solid #D1D5DB;
172+
flex: 1 0 auto;
173+
height: .3rem;
174+
}
175+
176+
.divider {
177+
color: #a4a6a8;
178+
margin: 0 1rem;
179+
font-size: 0.8rem;
180+
}
181+
149182
.subtitle {
150183
font-weight: bold;
151184
margin-top: 20px;

util/helper-functions.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,25 @@ export function refactorFlowWithMoreMessages(flow: any) {
3737
return flow;
3838
}
3939

40-
export function prepareFirstLastNameAsRequired(firstNameIdx: number, lastNameIdx: number, initialFlow: any) {
41-
if (initialFlow.ui.nodes[firstNameIdx].attributes.name === "traits.name.first") {
42-
initialFlow.ui.nodes[firstNameIdx].attributes.required = true
40+
export function prepareNodes(flow: any) {
41+
42+
let firstNameNode = flow.ui.nodes.find((node: any) => node.attributes?.name === "traits.name.first");
43+
if (firstNameNode) {
44+
firstNameNode.attributes.required = true
45+
}
46+
47+
let lastNameNode = flow.ui.nodes.find((node: any) => node.attributes?.name === "traits.name.last");
48+
if (lastNameNode) {
49+
lastNameNode.attributes.required = true
4350
}
44-
if (initialFlow.ui.nodes[lastNameIdx].attributes.name === "traits.name.last") {
45-
initialFlow.ui.nodes[lastNameIdx].attributes.required = true
51+
52+
let filteredNodes = flow.ui.nodes.filter((node: any) => !node.attributes?.name.startsWith("metadata"));
53+
54+
const providerId = flow.identity?.metadata_public?.registration_scope?.provider_id;
55+
if (["microsoft", "google"].includes(providerId)) {
56+
const mailNode = filteredNodes.find((node: any) => node.attributes?.name == "traits.email");
57+
mailNode.attributes.type = "hidden"
4658
}
47-
return initialFlow.ui.nodes;
59+
60+
return filteredNodes;
4861
}

0 commit comments

Comments
 (0)