Plug sign in steps + sso login URL check
Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<02b9c661a564d47f4821ff67a4dae9b0>>
|
* @generated SignedSource<<f242c4003408e8b86541c8f98b2c6d18>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -13,19 +13,19 @@ export type SignInInput = {
|
|||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
export type SignInPageMutation$variables = {
|
export type PasswordSignInPageMutation$variables = {
|
||||||
input: SignInInput;
|
input: SignInInput;
|
||||||
};
|
};
|
||||||
export type SignInPageMutation$data = {
|
export type PasswordSignInPageMutation$data = {
|
||||||
readonly signIn: {
|
readonly signIn: {
|
||||||
readonly session: {
|
readonly session: {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
} | null | undefined;
|
} | null | undefined;
|
||||||
} | null | undefined;
|
} | null | undefined;
|
||||||
};
|
};
|
||||||
export type SignInPageMutation = {
|
export type PasswordSignInPageMutation = {
|
||||||
response: SignInPageMutation$data;
|
response: PasswordSignInPageMutation$data;
|
||||||
variables: SignInPageMutation$variables;
|
variables: PasswordSignInPageMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -78,7 +78,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "SignInPageMutation",
|
"name": "PasswordSignInPageMutation",
|
||||||
"selections": (v1/*: any*/),
|
"selections": (v1/*: any*/),
|
||||||
"type": "Mutation",
|
"type": "Mutation",
|
||||||
"abstractKey": null
|
"abstractKey": null
|
||||||
@@ -87,20 +87,20 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "SignInPageMutation",
|
"name": "PasswordSignInPageMutation",
|
||||||
"selections": (v1/*: any*/)
|
"selections": (v1/*: any*/)
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "8e13b6ebdfd4ca6bde158dff53f596db",
|
"cacheID": "f60c1dcde17bef06ff8170d14c077dae",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "SignInPageMutation",
|
"name": "PasswordSignInPageMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation SignInPageMutation(\n $input: SignInInput!\n) {\n signIn(input: $input) {\n session {\n id\n }\n }\n}\n"
|
"text": "mutation PasswordSignInPageMutation(\n $input: SignInInput!\n) {\n signIn(input: $input) {\n session {\n id\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "6424104df106b64a6237d92299239a63";
|
(node as any).hash = "9b9b3de59d38ae7c288348c6bfc01afa";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
83
apps/console/src/__generated__/iam/SSOSignInPageQuery.graphql.ts
generated
Normal file
83
apps/console/src/__generated__/iam/SSOSignInPageQuery.graphql.ts
generated
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<44e9b1516c9cd0ae8419f326791e09e1>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
import { Result } from "relay-runtime";
|
||||||
|
export type SSOSignInPageQuery$variables = {
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
export type SSOSignInPageQuery$data = {
|
||||||
|
readonly ssoLoginURL: Result<string | null | undefined, unknown>;
|
||||||
|
};
|
||||||
|
export type SSOSignInPageQuery = {
|
||||||
|
response: SSOSignInPageQuery$data;
|
||||||
|
variables: SSOSignInPageQuery$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "email"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "email",
|
||||||
|
"variableName": "email"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "ssoLoginURL",
|
||||||
|
"storageKey": null
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "SSOSignInPageQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"kind": "CatchField",
|
||||||
|
"field": (v1/*: any*/),
|
||||||
|
"to": "RESULT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Query",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "SSOSignInPageQuery",
|
||||||
|
"selections": [
|
||||||
|
(v1/*: any*/)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "9b7766bbd9f160dc8bbcf34705641486",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "SSOSignInPageQuery",
|
||||||
|
"operationKind": "query",
|
||||||
|
"text": "query SSOSignInPageQuery(\n $email: EmailAddr!\n) {\n ssoLoginURL(email: $email)\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "5749c48e67881c71bbc99a9e1120767f";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import { Button, Field, useToast } from "@probo/ui";
|
import { Button, Field, IconChevronLeft, useToast } from "@probo/ui";
|
||||||
import type { FormEventHandler } from "react";
|
import type { FormEventHandler } from "react";
|
||||||
import { useMutation } from "react-relay";
|
import { useMutation } from "react-relay";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
import type { SignInPageMutation } from "/__generated__/iam/SignInPageMutation.graphql";
|
import type { PasswordSignInPageMutation } from "/__generated__/iam/PasswordSignInPageMutation.graphql";
|
||||||
|
|
||||||
const signInMutation = graphql`
|
const signInMutation = graphql`
|
||||||
mutation SignInPageMutation($input: SignInInput!) {
|
mutation PasswordSignInPageMutation($input: SignInInput!) {
|
||||||
signIn(input: $input) {
|
signIn(input: $input) {
|
||||||
session {
|
session {
|
||||||
id
|
id
|
||||||
@@ -16,13 +16,13 @@ const signInMutation = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// TODO initial screen + SAML login + after sign in URL state
|
export default function PasswordSignInPage() {
|
||||||
export default function SignInPage() {
|
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const [signIn, isSigningIn] = useMutation<SignInPageMutation>(signInMutation);
|
const [signIn, isSigningIn] =
|
||||||
|
useMutation<PasswordSignInPageMutation>(signInMutation);
|
||||||
|
|
||||||
const handlePasswordLogin: FormEventHandler<HTMLFormElement> = (e) => {
|
const handlePasswordLogin: FormEventHandler<HTMLFormElement> = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -54,14 +54,13 @@ export default function SignInPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="space-y-4" onSubmit={handlePasswordLogin}>
|
<form className="space-y-4" onSubmit={handlePasswordLogin}>
|
||||||
{/* <button
|
<Link
|
||||||
type="button"
|
to="/auth/login"
|
||||||
onClick={handleBack}
|
|
||||||
className="flex items-center gap-2 text-txt-secondary hover:text-txt-primary transition-colors mb-4"
|
className="flex items-center gap-2 text-txt-secondary hover:text-txt-primary transition-colors mb-4"
|
||||||
>
|
>
|
||||||
<IconChevronLeft size={20} />
|
<IconChevronLeft size={20} />
|
||||||
<span className="text-sm">{__("Back")}</span>
|
<span className="text-sm">{__("Back")}</span>
|
||||||
</button> */}
|
</Link>
|
||||||
|
|
||||||
<h1 className="text-center text-2xl font-bold">
|
<h1 className="text-center text-2xl font-bold">
|
||||||
{__("Login with Email")}
|
{__("Login with Email")}
|
||||||
133
apps/console/src/pages/iam/auth/sign-in/SSOSignInPage.tsx
Normal file
133
apps/console/src/pages/iam/auth/sign-in/SSOSignInPage.tsx
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { useTranslate } from "@probo/i18n";
|
||||||
|
import { Button, Field, IconChevronLeft, useToast } from "@probo/ui";
|
||||||
|
import { useEffect, useState, type FormEventHandler } from "react";
|
||||||
|
import {
|
||||||
|
usePreloadedQuery,
|
||||||
|
useQueryLoader,
|
||||||
|
type PreloadedQuery,
|
||||||
|
} from "react-relay";
|
||||||
|
import { Link, useNavigate } from "react-router";
|
||||||
|
import { graphql } from "relay-runtime";
|
||||||
|
import type { SSOSignInPageQuery } from "/__generated__/iam/SSOSignInPageQuery.graphql";
|
||||||
|
|
||||||
|
const ssoAvailabilityQuery = graphql`
|
||||||
|
query SSOSignInPageQuery($email: EmailAddr!) {
|
||||||
|
ssoLoginURL(email: $email) @catch(to: RESULT)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function SSOSignInPage() {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
|
||||||
|
const [queryRef, loadQuery] =
|
||||||
|
useQueryLoader<SSOSignInPageQuery>(ssoAvailabilityQuery);
|
||||||
|
const [checking, setChecking] = useState(false);
|
||||||
|
|
||||||
|
const handleSSOCheck: FormEventHandler<HTMLFormElement> = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setChecking(true);
|
||||||
|
const formData = new FormData(e.currentTarget);
|
||||||
|
const email = formData.get("email")?.toString();
|
||||||
|
|
||||||
|
if (!email) return;
|
||||||
|
|
||||||
|
loadQuery({ email }, { fetchPolicy: "network-only" });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<form className="space-y-4" onSubmit={handleSSOCheck}>
|
||||||
|
<Link
|
||||||
|
to="/auth/login"
|
||||||
|
className="flex items-center gap-2 text-txt-secondary hover:text-txt-primary transition-colors mb-4"
|
||||||
|
>
|
||||||
|
<IconChevronLeft size={20} />
|
||||||
|
<span className="text-sm">{__("Back")}</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<h1 className="text-center text-2xl font-bold">
|
||||||
|
{__("Login with SSO")}
|
||||||
|
</h1>
|
||||||
|
<p className="text-center text-txt-tertiary mt-1 mb-6">
|
||||||
|
{__("Enter your work email to continue with SSO")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
required
|
||||||
|
placeholder={__("Work Email")}
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
label={__("Work Email")}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button className="w-full" disabled={checking}>
|
||||||
|
{checking ? __("Checking...") : __("Continue with SSO")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="text-center mt-6 text-sm text-txt-secondary">
|
||||||
|
{__("Don't have an account ?")}{" "}
|
||||||
|
<Link
|
||||||
|
to="/auth/register"
|
||||||
|
className="underline hover:text-txt-primary"
|
||||||
|
>
|
||||||
|
{__("Register")}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{queryRef && (
|
||||||
|
<NavigateToSSOLoginURL
|
||||||
|
onSSOAvailabilityCheck={setChecking}
|
||||||
|
queryRef={queryRef}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigateToSSOLoginURL(props: {
|
||||||
|
queryRef: PreloadedQuery<SSOSignInPageQuery>;
|
||||||
|
onSSOAvailabilityCheck: (checking: boolean) => void;
|
||||||
|
}) {
|
||||||
|
const { queryRef } = props;
|
||||||
|
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { ssoLoginURL } = usePreloadedQuery<SSOSignInPageQuery>(
|
||||||
|
ssoAvailabilityQuery,
|
||||||
|
queryRef,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ssoLoginURL.ok) {
|
||||||
|
console.log(ssoLoginURL);
|
||||||
|
toast({
|
||||||
|
title: __("Error"),
|
||||||
|
description:
|
||||||
|
ssoLoginURL.errors[0] instanceof Error
|
||||||
|
? ssoLoginURL.errors[0].message
|
||||||
|
: __("SSO not available for this email domain"),
|
||||||
|
variant: "error",
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate("/auth/login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ssoLoginURL.value) {
|
||||||
|
toast({
|
||||||
|
title: __("Error"),
|
||||||
|
description: __("SSO not available for this email domain"),
|
||||||
|
variant: "error",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = ssoLoginURL.value;
|
||||||
|
}, [__, navigate, ssoLoginURL, toast]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
57
apps/console/src/pages/iam/auth/sign-in/SignInPage.tsx
Normal file
57
apps/console/src/pages/iam/auth/sign-in/SignInPage.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { useTranslate } from "@probo/i18n";
|
||||||
|
import { Button } from "@probo/ui";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
|
||||||
|
export default function SignInPage() {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h1 className="text-center text-2xl font-bold">
|
||||||
|
{__("Login to your account")}
|
||||||
|
</h1>
|
||||||
|
<p className="text-center text-txt-tertiary mt-1 mb-6">
|
||||||
|
{__("Choose your login method")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Button className="w-full" to="/auth/password-login">
|
||||||
|
{__("Login with Email")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="relative my-6">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<div className="w-full border-t border-border"></div>
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center">
|
||||||
|
<span
|
||||||
|
className="px-4 text-xs uppercase text-txt-secondary"
|
||||||
|
style={{ backgroundColor: "var(--color-level-0)" }}
|
||||||
|
>
|
||||||
|
{__("Or")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button variant="secondary" className="w-full" to="/auth/sso-login">
|
||||||
|
{__("Login with SSO")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="text-center mt-6 text-sm text-txt-secondary">
|
||||||
|
{__("Don't have an account ?")}{" "}
|
||||||
|
<Link to="/auth/register" className="underline hover:text-txt-primary">
|
||||||
|
{__("Register")}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center text-sm text-txt-secondary">
|
||||||
|
{__("Forgot password?")}{" "}
|
||||||
|
<Link
|
||||||
|
to="/auth/forgot-password"
|
||||||
|
className="underline hover:text-txt-primary"
|
||||||
|
>
|
||||||
|
{__("Reset password")}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -65,7 +65,17 @@ const routes = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "login",
|
path: "login",
|
||||||
Component: lazy(() => import("./pages/iam/auth/SignInPage")),
|
Component: lazy(() => import("./pages/iam/auth/sign-in/SignInPage")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "password-login",
|
||||||
|
Component: lazy(
|
||||||
|
() => import("./pages/iam/auth/sign-in/PasswordSignInPage"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "sso-login",
|
||||||
|
Component: lazy(() => import("./pages/iam/auth/sign-in/SSOSignInPage")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "register",
|
path: "register",
|
||||||
@@ -78,7 +88,7 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: "signup-from-invitation",
|
path: "signup-from-invitation",
|
||||||
Component: lazy(
|
Component: lazy(
|
||||||
() => import("./pages/iam/auth/SignUpFromInvitationPage")
|
() => import("./pages/iam/auth/SignUpFromInvitationPage"),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -100,13 +110,13 @@ const routes = [
|
|||||||
{
|
{
|
||||||
index: true,
|
index: true,
|
||||||
Component: lazy(
|
Component: lazy(
|
||||||
() => import("./pages/iam/memberships/MembershipsPageLoader")
|
() => import("./pages/iam/memberships/MembershipsPageLoader"),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "organizations/new",
|
path: "organizations/new",
|
||||||
Component: lazy(
|
Component: lazy(
|
||||||
() => import("./pages/iam/organizations/NewOrganizationPage")
|
() => import("./pages/iam/organizations/NewOrganizationPage"),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -123,7 +133,7 @@ const routes = [
|
|||||||
path: "/organizations/:organizationId/employee",
|
path: "/organizations/:organizationId/employee",
|
||||||
Fallback: () => "fallback employee...",
|
Fallback: () => "fallback employee...",
|
||||||
Component: lazy(
|
Component: lazy(
|
||||||
() => import("./pages/organizations/employee/EmployeeLayoutLoader")
|
() => import("./pages/organizations/employee/EmployeeLayoutLoader"),
|
||||||
),
|
),
|
||||||
ErrorBoundary: ErrorBoundary,
|
ErrorBoundary: ErrorBoundary,
|
||||||
children: [
|
children: [
|
||||||
@@ -132,7 +142,7 @@ const routes = [
|
|||||||
// Component: () => "hello world",
|
// Component: () => "hello world",
|
||||||
Component: lazy(
|
Component: lazy(
|
||||||
() =>
|
() =>
|
||||||
import("./pages/organizations/employee/EmployeeDocumentsPageLoader")
|
import("./pages/organizations/employee/EmployeeDocumentsPageLoader"),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -140,7 +150,7 @@ const routes = [
|
|||||||
ErrorBoundary: ErrorBoundary,
|
ErrorBoundary: ErrorBoundary,
|
||||||
Component: lazy(
|
Component: lazy(
|
||||||
() =>
|
() =>
|
||||||
import("./pages/organizations/employee/EmployeeDocumentSignaturePageLoader")
|
import("./pages/organizations/employee/EmployeeDocumentSignaturePageLoader"),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -148,7 +158,7 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: "/organizations/:organizationId",
|
path: "/organizations/:organizationId",
|
||||||
Component: lazy(
|
Component: lazy(
|
||||||
() => import("./pages/iam/memberships/MembershipLayoutLoader")
|
() => import("./pages/iam/memberships/MembershipLayoutLoader"),
|
||||||
),
|
),
|
||||||
ErrorBoundary: ErrorBoundary,
|
ErrorBoundary: ErrorBoundary,
|
||||||
children: [
|
children: [
|
||||||
@@ -170,7 +180,7 @@ const routes = [
|
|||||||
path: "settings",
|
path: "settings",
|
||||||
Fallback: PageSkeleton,
|
Fallback: PageSkeleton,
|
||||||
Component: lazy(
|
Component: lazy(
|
||||||
() => import("./pages/iam/organizations/settings/SettingsLayout")
|
() => import("./pages/iam/organizations/settings/SettingsLayout"),
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
@@ -183,35 +193,35 @@ const routes = [
|
|||||||
path: "general",
|
path: "general",
|
||||||
Component: lazy(
|
Component: lazy(
|
||||||
() =>
|
() =>
|
||||||
import("./pages/iam/organizations/settings/GeneralSettingsPageLoader")
|
import("./pages/iam/organizations/settings/GeneralSettingsPageLoader"),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "members",
|
path: "members",
|
||||||
Component: lazy(
|
Component: lazy(
|
||||||
() =>
|
() =>
|
||||||
import("./pages/iam/organizations/settings/MembersPageLoader")
|
import("./pages/iam/organizations/settings/MembersPageLoader"),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "domain",
|
path: "domain",
|
||||||
Component: lazy(
|
Component: lazy(
|
||||||
() =>
|
() =>
|
||||||
import("./pages/organizations/settings/DomainSettingsPageLoader")
|
import("./pages/organizations/settings/DomainSettingsPageLoader"),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "saml-sso",
|
path: "saml-sso",
|
||||||
Component: lazy(
|
Component: lazy(
|
||||||
() =>
|
() =>
|
||||||
import("./pages/iam/organizations/settings/SAMLSettingsPageLoader")
|
import("./pages/iam/organizations/settings/SAMLSettingsPageLoader"),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "scim",
|
path: "scim",
|
||||||
Component: lazy(
|
Component: lazy(
|
||||||
() =>
|
() =>
|
||||||
import("./pages/iam/organizations/settings/SCIMSettingsPageLoader")
|
import("./pages/iam/organizations/settings/SCIMSettingsPageLoader"),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -513,6 +513,73 @@ WHERE
|
|||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SAMLConfigurations) LoadVerifiedByEmailDomain(ctx context.Context, conn pg.Conn, emailDomain string) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
organization_id,
|
||||||
|
email_domain,
|
||||||
|
enforcement_policy,
|
||||||
|
idp_entity_id,
|
||||||
|
idp_sso_url,
|
||||||
|
idp_certificate,
|
||||||
|
idp_metadata_url,
|
||||||
|
attribute_email,
|
||||||
|
attribute_firstname,
|
||||||
|
attribute_lastname,
|
||||||
|
attribute_role,
|
||||||
|
auto_signup_enabled,
|
||||||
|
domain_verification_token,
|
||||||
|
domain_verified_at,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
iam_saml_configurations
|
||||||
|
WHERE
|
||||||
|
email_domain = @email_domain
|
||||||
|
AND domain_verified_at IS NOT NULL
|
||||||
|
ORDER BY email_domain ASC;
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, pgx.StrictNamedArgs{"email_domain": emailDomain})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query iam_saml_configurations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
samlConfigurations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[SAMLConfiguration])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect saml_configurations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*s = samlConfigurations
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SAMLConfigurations) CountVerifiedByEmailDomain(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
emailDomain string,
|
||||||
|
) (int, error) {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
COUNT(*)
|
||||||
|
FROM
|
||||||
|
iam_saml_configurations
|
||||||
|
WHERE
|
||||||
|
email_domain = @email_domain
|
||||||
|
AND domain_verified_at IS NOT NULL
|
||||||
|
`
|
||||||
|
|
||||||
|
row := conn.QueryRow(ctx, q, pgx.StrictNamedArgs{"email_domain": emailDomain})
|
||||||
|
var count int
|
||||||
|
if err := row.Scan(&count); err != nil {
|
||||||
|
return 0, fmt.Errorf("cannot count SAML configurations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SAMLConfigurations) LoadUnverified(
|
func (s *SAMLConfigurations) LoadUnverified(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
|
|||||||
@@ -856,3 +856,54 @@ func (s AccountService) GetProfileForMembership(ctx context.Context, membershipI
|
|||||||
|
|
||||||
return profile, nil
|
return profile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s AccountService) ListSAMLConfigurationsForEmail(
|
||||||
|
ctx context.Context,
|
||||||
|
email mail.Addr,
|
||||||
|
) (coredata.SAMLConfigurations, error) {
|
||||||
|
samlConfigurations := coredata.SAMLConfigurations{}
|
||||||
|
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
err := samlConfigurations.LoadVerifiedByEmailDomain(ctx, conn, email.Domain())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot load saml configurations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return samlConfigurations, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s AccountService) CountSAMLConfigurationsForEmail(
|
||||||
|
ctx context.Context,
|
||||||
|
email mail.Addr,
|
||||||
|
) (int, error) {
|
||||||
|
var (
|
||||||
|
count int
|
||||||
|
samlConfigurations coredata.SAMLConfigurations
|
||||||
|
)
|
||||||
|
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) (err error) {
|
||||||
|
count, err = samlConfigurations.CountVerifiedByEmailDomain(ctx, conn, email.Domain())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot count saml configurations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ interface Node {
|
|||||||
type Query {
|
type Query {
|
||||||
node(id: ID!): Node @session(required: PRESENT)
|
node(id: ID!): Node @session(required: PRESENT)
|
||||||
viewer: Identity @session(required: PRESENT)
|
viewer: Identity @session(required: PRESENT)
|
||||||
checkSSOAvailability(email: String!): SSOAvailability!
|
ssoLoginURL(email: EmailAddr!): String
|
||||||
|
@goField(forceResolver: true)
|
||||||
@session(required: NONE)
|
@session(required: NONE)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,12 +350,6 @@ type SAMLAttributeMappings {
|
|||||||
role: String!
|
role: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type SSOAvailability {
|
|
||||||
available: Boolean!
|
|
||||||
samlConfigId: ID
|
|
||||||
organizationId: ID
|
|
||||||
}
|
|
||||||
|
|
||||||
type SCIMConfiguration implements Node {
|
type SCIMConfiguration implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
endpointUrl: String! @goField(forceResolver: true)
|
endpointUrl: String! @goField(forceResolver: true)
|
||||||
|
|||||||
@@ -294,9 +294,9 @@ type ComplexityRoot struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Query struct {
|
Query struct {
|
||||||
CheckSSOAvailability func(childComplexity int, email string) int
|
Node func(childComplexity int, id gid.GID) int
|
||||||
Node func(childComplexity int, id gid.GID) int
|
SsoLoginURL func(childComplexity int, email mail.Addr) int
|
||||||
Viewer func(childComplexity int) int
|
Viewer func(childComplexity int) int
|
||||||
}
|
}
|
||||||
|
|
||||||
RegenerateSCIMTokenPayload struct {
|
RegenerateSCIMTokenPayload struct {
|
||||||
@@ -399,12 +399,6 @@ type ComplexityRoot struct {
|
|||||||
Node func(childComplexity int) int
|
Node func(childComplexity int) int
|
||||||
}
|
}
|
||||||
|
|
||||||
SSOAvailability struct {
|
|
||||||
Available func(childComplexity int) int
|
|
||||||
OrganizationID func(childComplexity int) int
|
|
||||||
SamlConfigID func(childComplexity int) int
|
|
||||||
}
|
|
||||||
|
|
||||||
Session struct {
|
Session struct {
|
||||||
CreatedAt func(childComplexity int) int
|
CreatedAt func(childComplexity int) int
|
||||||
ExpiresAt func(childComplexity int) int
|
ExpiresAt func(childComplexity int) int
|
||||||
@@ -541,7 +535,7 @@ type PersonalAPIKeyConnectionResolver interface {
|
|||||||
type QueryResolver interface {
|
type QueryResolver interface {
|
||||||
Node(ctx context.Context, id gid.GID) (types.Node, error)
|
Node(ctx context.Context, id gid.GID) (types.Node, error)
|
||||||
Viewer(ctx context.Context) (*types.Identity, error)
|
Viewer(ctx context.Context) (*types.Identity, error)
|
||||||
CheckSSOAvailability(ctx context.Context, email string) (*types.SSOAvailability, error)
|
SsoLoginURL(ctx context.Context, email mail.Addr) (*string, error)
|
||||||
}
|
}
|
||||||
type SAMLConfigurationResolver interface {
|
type SAMLConfigurationResolver interface {
|
||||||
TestLoginURL(ctx context.Context, obj *types.SAMLConfiguration) (string, error)
|
TestLoginURL(ctx context.Context, obj *types.SAMLConfiguration) (string, error)
|
||||||
@@ -1593,17 +1587,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
|||||||
|
|
||||||
return e.complexity.PersonalAPIKeyEdge.Node(childComplexity), true
|
return e.complexity.PersonalAPIKeyEdge.Node(childComplexity), true
|
||||||
|
|
||||||
case "Query.checkSSOAvailability":
|
|
||||||
if e.complexity.Query.CheckSSOAvailability == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
args, err := ec.field_Query_checkSSOAvailability_args(ctx, rawArgs)
|
|
||||||
if err != nil {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.complexity.Query.CheckSSOAvailability(childComplexity, args["email"].(string)), true
|
|
||||||
case "Query.node":
|
case "Query.node":
|
||||||
if e.complexity.Query.Node == nil {
|
if e.complexity.Query.Node == nil {
|
||||||
break
|
break
|
||||||
@@ -1615,6 +1598,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
return e.complexity.Query.Node(childComplexity, args["id"].(gid.GID)), true
|
return e.complexity.Query.Node(childComplexity, args["id"].(gid.GID)), true
|
||||||
|
case "Query.ssoLoginURL":
|
||||||
|
if e.complexity.Query.SsoLoginURL == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
args, err := ec.field_Query_ssoLoginURL_args(ctx, rawArgs)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.Query.SsoLoginURL(childComplexity, args["email"].(mail.Addr)), true
|
||||||
case "Query.viewer":
|
case "Query.viewer":
|
||||||
if e.complexity.Query.Viewer == nil {
|
if e.complexity.Query.Viewer == nil {
|
||||||
break
|
break
|
||||||
@@ -1987,25 +1981,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
|||||||
|
|
||||||
return e.complexity.SCIMEventEdge.Node(childComplexity), true
|
return e.complexity.SCIMEventEdge.Node(childComplexity), true
|
||||||
|
|
||||||
case "SSOAvailability.available":
|
|
||||||
if e.complexity.SSOAvailability.Available == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.complexity.SSOAvailability.Available(childComplexity), true
|
|
||||||
case "SSOAvailability.organizationId":
|
|
||||||
if e.complexity.SSOAvailability.OrganizationID == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.complexity.SSOAvailability.OrganizationID(childComplexity), true
|
|
||||||
case "SSOAvailability.samlConfigId":
|
|
||||||
if e.complexity.SSOAvailability.SamlConfigID == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.complexity.SSOAvailability.SamlConfigID(childComplexity), true
|
|
||||||
|
|
||||||
case "Session.createdAt":
|
case "Session.createdAt":
|
||||||
if e.complexity.Session.CreatedAt == nil {
|
if e.complexity.Session.CreatedAt == nil {
|
||||||
break
|
break
|
||||||
@@ -2347,7 +2322,8 @@ interface Node {
|
|||||||
type Query {
|
type Query {
|
||||||
node(id: ID!): Node @session(required: PRESENT)
|
node(id: ID!): Node @session(required: PRESENT)
|
||||||
viewer: Identity @session(required: PRESENT)
|
viewer: Identity @session(required: PRESENT)
|
||||||
checkSSOAvailability(email: String!): SSOAvailability!
|
ssoLoginURL(email: EmailAddr!): String
|
||||||
|
@goField(forceResolver: true)
|
||||||
@session(required: NONE)
|
@session(required: NONE)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2642,12 +2618,6 @@ type SAMLAttributeMappings {
|
|||||||
role: String!
|
role: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type SSOAvailability {
|
|
||||||
available: Boolean!
|
|
||||||
samlConfigId: ID
|
|
||||||
organizationId: ID
|
|
||||||
}
|
|
||||||
|
|
||||||
type SCIMConfiguration implements Node {
|
type SCIMConfiguration implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
endpointUrl: String! @goField(forceResolver: true)
|
endpointUrl: String! @goField(forceResolver: true)
|
||||||
@@ -3779,17 +3749,6 @@ func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs
|
|||||||
return args, nil
|
return args, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) field_Query_checkSSOAvailability_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
|
||||||
var err error
|
|
||||||
args := map[string]any{}
|
|
||||||
arg0, err := graphql.ProcessArgField(ctx, rawArgs, "email", ec.unmarshalNString2string)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
args["email"] = arg0
|
|
||||||
return args, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) field_Query_node_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
func (ec *executionContext) field_Query_node_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
||||||
var err error
|
var err error
|
||||||
args := map[string]any{}
|
args := map[string]any{}
|
||||||
@@ -3801,6 +3760,17 @@ func (ec *executionContext) field_Query_node_args(ctx context.Context, rawArgs m
|
|||||||
return args, nil
|
return args, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) field_Query_ssoLoginURL_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
||||||
|
var err error
|
||||||
|
args := map[string]any{}
|
||||||
|
arg0, err := graphql.ProcessArgField(ctx, rawArgs, "email", ec.unmarshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
args["email"] = arg0
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) field_SAMLConfiguration_permission_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
func (ec *executionContext) field_SAMLConfiguration_permission_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
||||||
var err error
|
var err error
|
||||||
args := map[string]any{}
|
args := map[string]any{}
|
||||||
@@ -9531,15 +9501,15 @@ func (ec *executionContext) fieldContext_Query_viewer(_ context.Context, field g
|
|||||||
return fc, nil
|
return fc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _Query_checkSSOAvailability(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Query_ssoLoginURL(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
return graphql.ResolveField(
|
return graphql.ResolveField(
|
||||||
ctx,
|
ctx,
|
||||||
ec.OperationContext,
|
ec.OperationContext,
|
||||||
field,
|
field,
|
||||||
ec.fieldContext_Query_checkSSOAvailability,
|
ec.fieldContext_Query_ssoLoginURL,
|
||||||
func(ctx context.Context) (any, error) {
|
func(ctx context.Context) (any, error) {
|
||||||
fc := graphql.GetFieldContext(ctx)
|
fc := graphql.GetFieldContext(ctx)
|
||||||
return ec.resolvers.Query().CheckSSOAvailability(ctx, fc.Args["email"].(string))
|
return ec.resolvers.Query().SsoLoginURL(ctx, fc.Args["email"].(mail.Addr))
|
||||||
},
|
},
|
||||||
func(ctx context.Context, next graphql.Resolver) graphql.Resolver {
|
func(ctx context.Context, next graphql.Resolver) graphql.Resolver {
|
||||||
directive0 := next
|
directive0 := next
|
||||||
@@ -9547,11 +9517,11 @@ func (ec *executionContext) _Query_checkSSOAvailability(ctx context.Context, fie
|
|||||||
directive1 := func(ctx context.Context) (any, error) {
|
directive1 := func(ctx context.Context) (any, error) {
|
||||||
required, err := ec.unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐSessionRequirement(ctx, "NONE")
|
required, err := ec.unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐSessionRequirement(ctx, "NONE")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var zeroVal *types.SSOAvailability
|
var zeroVal *string
|
||||||
return zeroVal, err
|
return zeroVal, err
|
||||||
}
|
}
|
||||||
if ec.directives.Session == nil {
|
if ec.directives.Session == nil {
|
||||||
var zeroVal *types.SSOAvailability
|
var zeroVal *string
|
||||||
return zeroVal, errors.New("directive session is not implemented")
|
return zeroVal, errors.New("directive session is not implemented")
|
||||||
}
|
}
|
||||||
return ec.directives.Session(ctx, nil, directive0, required)
|
return ec.directives.Session(ctx, nil, directive0, required)
|
||||||
@@ -9560,28 +9530,20 @@ func (ec *executionContext) _Query_checkSSOAvailability(ctx context.Context, fie
|
|||||||
next = directive1
|
next = directive1
|
||||||
return next
|
return next
|
||||||
},
|
},
|
||||||
ec.marshalNSSOAvailability2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐSSOAvailability,
|
ec.marshalOString2ᚖstring,
|
||||||
true,
|
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) fieldContext_Query_checkSSOAvailability(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
func (ec *executionContext) fieldContext_Query_ssoLoginURL(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
fc = &graphql.FieldContext{
|
fc = &graphql.FieldContext{
|
||||||
Object: "Query",
|
Object: "Query",
|
||||||
Field: field,
|
Field: field,
|
||||||
IsMethod: true,
|
IsMethod: true,
|
||||||
IsResolver: true,
|
IsResolver: true,
|
||||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
switch field.Name {
|
return nil, errors.New("field of type String does not have child fields")
|
||||||
case "available":
|
|
||||||
return ec.fieldContext_SSOAvailability_available(ctx, field)
|
|
||||||
case "samlConfigId":
|
|
||||||
return ec.fieldContext_SSOAvailability_samlConfigId(ctx, field)
|
|
||||||
case "organizationId":
|
|
||||||
return ec.fieldContext_SSOAvailability_organizationId(ctx, field)
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("no field named %q was found under type SSOAvailability", field.Name)
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -9591,7 +9553,7 @@ func (ec *executionContext) fieldContext_Query_checkSSOAvailability(ctx context.
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
ctx = graphql.WithFieldContext(ctx, fc)
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
if fc.Args, err = ec.field_Query_checkSSOAvailability_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
|
if fc.Args, err = ec.field_Query_ssoLoginURL_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
|
||||||
ec.Error(ctx, err)
|
ec.Error(ctx, err)
|
||||||
return fc, err
|
return fc, err
|
||||||
}
|
}
|
||||||
@@ -11579,93 +11541,6 @@ func (ec *executionContext) fieldContext_SCIMEventEdge_cursor(_ context.Context,
|
|||||||
return fc, nil
|
return fc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _SSOAvailability_available(ctx context.Context, field graphql.CollectedField, obj *types.SSOAvailability) (ret graphql.Marshaler) {
|
|
||||||
return graphql.ResolveField(
|
|
||||||
ctx,
|
|
||||||
ec.OperationContext,
|
|
||||||
field,
|
|
||||||
ec.fieldContext_SSOAvailability_available,
|
|
||||||
func(ctx context.Context) (any, error) {
|
|
||||||
return obj.Available, nil
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
ec.marshalNBoolean2bool,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) fieldContext_SSOAvailability_available(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
|
||||||
fc = &graphql.FieldContext{
|
|
||||||
Object: "SSOAvailability",
|
|
||||||
Field: field,
|
|
||||||
IsMethod: false,
|
|
||||||
IsResolver: false,
|
|
||||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
|
||||||
return nil, errors.New("field of type Boolean does not have child fields")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return fc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) _SSOAvailability_samlConfigId(ctx context.Context, field graphql.CollectedField, obj *types.SSOAvailability) (ret graphql.Marshaler) {
|
|
||||||
return graphql.ResolveField(
|
|
||||||
ctx,
|
|
||||||
ec.OperationContext,
|
|
||||||
field,
|
|
||||||
ec.fieldContext_SSOAvailability_samlConfigId,
|
|
||||||
func(ctx context.Context) (any, error) {
|
|
||||||
return obj.SamlConfigID, nil
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
ec.marshalOID2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) fieldContext_SSOAvailability_samlConfigId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
|
||||||
fc = &graphql.FieldContext{
|
|
||||||
Object: "SSOAvailability",
|
|
||||||
Field: field,
|
|
||||||
IsMethod: false,
|
|
||||||
IsResolver: false,
|
|
||||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
|
||||||
return nil, errors.New("field of type ID does not have child fields")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return fc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) _SSOAvailability_organizationId(ctx context.Context, field graphql.CollectedField, obj *types.SSOAvailability) (ret graphql.Marshaler) {
|
|
||||||
return graphql.ResolveField(
|
|
||||||
ctx,
|
|
||||||
ec.OperationContext,
|
|
||||||
field,
|
|
||||||
ec.fieldContext_SSOAvailability_organizationId,
|
|
||||||
func(ctx context.Context) (any, error) {
|
|
||||||
return obj.OrganizationID, nil
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
ec.marshalOID2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) fieldContext_SSOAvailability_organizationId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
|
||||||
fc = &graphql.FieldContext{
|
|
||||||
Object: "SSOAvailability",
|
|
||||||
Field: field,
|
|
||||||
IsMethod: false,
|
|
||||||
IsResolver: false,
|
|
||||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
|
||||||
return nil, errors.New("field of type ID does not have child fields")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return fc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) _Session_id(ctx context.Context, field graphql.CollectedField, obj *types.Session) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Session_id(ctx context.Context, field graphql.CollectedField, obj *types.Session) (ret graphql.Marshaler) {
|
||||||
return graphql.ResolveField(
|
return graphql.ResolveField(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -17799,19 +17674,16 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
|
|||||||
}
|
}
|
||||||
|
|
||||||
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
|
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
|
||||||
case "checkSSOAvailability":
|
case "ssoLoginURL":
|
||||||
field := field
|
field := field
|
||||||
|
|
||||||
innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
|
innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
ec.Error(ctx, ec.Recover(ctx, r))
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
res = ec._Query_checkSSOAvailability(ctx, field)
|
res = ec._Query_ssoLoginURL(ctx, field)
|
||||||
if res == graphql.Null {
|
|
||||||
atomic.AddUint32(&fs.Invalids, 1)
|
|
||||||
}
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18917,49 +18789,6 @@ func (ec *executionContext) _SCIMEventEdge(ctx context.Context, sel ast.Selectio
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
var sSOAvailabilityImplementors = []string{"SSOAvailability"}
|
|
||||||
|
|
||||||
func (ec *executionContext) _SSOAvailability(ctx context.Context, sel ast.SelectionSet, obj *types.SSOAvailability) graphql.Marshaler {
|
|
||||||
fields := graphql.CollectFields(ec.OperationContext, sel, sSOAvailabilityImplementors)
|
|
||||||
|
|
||||||
out := graphql.NewFieldSet(fields)
|
|
||||||
deferred := make(map[string]*graphql.FieldSet)
|
|
||||||
for i, field := range fields {
|
|
||||||
switch field.Name {
|
|
||||||
case "__typename":
|
|
||||||
out.Values[i] = graphql.MarshalString("SSOAvailability")
|
|
||||||
case "available":
|
|
||||||
out.Values[i] = ec._SSOAvailability_available(ctx, field, obj)
|
|
||||||
if out.Values[i] == graphql.Null {
|
|
||||||
out.Invalids++
|
|
||||||
}
|
|
||||||
case "samlConfigId":
|
|
||||||
out.Values[i] = ec._SSOAvailability_samlConfigId(ctx, field, obj)
|
|
||||||
case "organizationId":
|
|
||||||
out.Values[i] = ec._SSOAvailability_organizationId(ctx, field, obj)
|
|
||||||
default:
|
|
||||||
panic("unknown field " + strconv.Quote(field.Name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out.Dispatch(ctx)
|
|
||||||
if out.Invalids > 0 {
|
|
||||||
return graphql.Null
|
|
||||||
}
|
|
||||||
|
|
||||||
atomic.AddInt32(&ec.deferred, int32(len(deferred)))
|
|
||||||
|
|
||||||
for label, dfs := range deferred {
|
|
||||||
ec.processDeferredGroup(graphql.DeferredGroup{
|
|
||||||
Label: label,
|
|
||||||
Path: graphql.GetPath(ctx),
|
|
||||||
FieldSet: dfs,
|
|
||||||
Context: ctx,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
var sessionImplementors = []string{"Session", "Node"}
|
var sessionImplementors = []string{"Session", "Node"}
|
||||||
|
|
||||||
func (ec *executionContext) _Session(ctx context.Context, sel ast.SelectionSet, obj *types.Session) graphql.Marshaler {
|
func (ec *executionContext) _Session(ctx context.Context, sel ast.SelectionSet, obj *types.Session) graphql.Marshaler {
|
||||||
@@ -20706,20 +20535,6 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ec *executionContext) marshalNSSOAvailability2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐSSOAvailability(ctx context.Context, sel ast.SelectionSet, v types.SSOAvailability) graphql.Marshaler {
|
|
||||||
return ec._SSOAvailability(ctx, sel, &v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) marshalNSSOAvailability2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐSSOAvailability(ctx context.Context, sel ast.SelectionSet, v *types.SSOAvailability) graphql.Marshaler {
|
|
||||||
if v == nil {
|
|
||||||
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
|
|
||||||
graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow")
|
|
||||||
}
|
|
||||||
return graphql.Null
|
|
||||||
}
|
|
||||||
return ec._SSOAvailability(ctx, sel, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) marshalNSession2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐSession(ctx context.Context, sel ast.SelectionSet, v *types.Session) graphql.Marshaler {
|
func (ec *executionContext) marshalNSession2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐSession(ctx context.Context, sel ast.SelectionSet, v *types.Session) graphql.Marshaler {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
|
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
|
||||||
@@ -21306,24 +21121,6 @@ func (ec *executionContext) marshalOForgotPasswordPayload2ᚖgoᚗproboᚗincᚋ
|
|||||||
return ec._ForgotPasswordPayload(ctx, sel, v)
|
return ec._ForgotPasswordPayload(ctx, sel, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) unmarshalOID2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx context.Context, v any) (*gid.GID, error) {
|
|
||||||
if v == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
res, err := gid1.UnmarshalGIDScalar(v)
|
|
||||||
return &res, graphql.ErrorOnPath(ctx, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) marshalOID2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx context.Context, sel ast.SelectionSet, v *gid.GID) graphql.Marshaler {
|
|
||||||
if v == nil {
|
|
||||||
return graphql.Null
|
|
||||||
}
|
|
||||||
_ = sel
|
|
||||||
_ = ctx
|
|
||||||
res := gid1.MarshalGIDScalar(*v)
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) marshalOIdentity2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐIdentity(ctx context.Context, sel ast.SelectionSet, v *types.Identity) graphql.Marshaler {
|
func (ec *executionContext) marshalOIdentity2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐIdentity(ctx context.Context, sel ast.SelectionSet, v *types.Identity) graphql.Marshaler {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
|
|||||||
@@ -430,12 +430,6 @@ type SCIMEventEdge struct {
|
|||||||
Cursor page.CursorKey `json:"cursor"`
|
Cursor page.CursorKey `json:"cursor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SSOAvailability struct {
|
|
||||||
Available bool `json:"available"`
|
|
||||||
SamlConfigID *gid.GID `json:"samlConfigId,omitempty"`
|
|
||||||
OrganizationID *gid.GID `json:"organizationId,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
ID gid.GID `json:"id"`
|
ID gid.GID `json:"id"`
|
||||||
Identity *Identity `json:"identity,omitempty"`
|
Identity *Identity `json:"identity,omitempty"`
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/99designs/gqlgen/graphql"
|
||||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||||
"go.gearno.de/kit/log"
|
"go.gearno.de/kit/log"
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
"go.probo.inc/probo/pkg/gid"
|
"go.probo.inc/probo/pkg/gid"
|
||||||
"go.probo.inc/probo/pkg/iam"
|
"go.probo.inc/probo/pkg/iam"
|
||||||
|
"go.probo.inc/probo/pkg/mail"
|
||||||
"go.probo.inc/probo/pkg/page"
|
"go.probo.inc/probo/pkg/page"
|
||||||
"go.probo.inc/probo/pkg/securecookie"
|
"go.probo.inc/probo/pkg/securecookie"
|
||||||
"go.probo.inc/probo/pkg/server/api/connect/v1/schema"
|
"go.probo.inc/probo/pkg/server/api/connect/v1/schema"
|
||||||
@@ -1532,9 +1534,43 @@ func (r *queryResolver) Viewer(ctx context.Context) (*types.Identity, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckSSOAvailability is the resolver for the checkSSOAvailability field.
|
// SsoLoginURL is the resolver for the ssoLoginURL field.
|
||||||
func (r *queryResolver) CheckSSOAvailability(ctx context.Context, email string) (*types.SSOAvailability, error) {
|
func (r *queryResolver) SsoLoginURL(ctx context.Context, email mail.Addr) (*string, error) {
|
||||||
panic(fmt.Errorf("not implemented: CheckSSOAvailability - checkSSOAvailability"))
|
count, err := r.iam.AccountService.CountSAMLConfigurationsForEmail(ctx, email)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot count SAML configurations for email", log.Error(err))
|
||||||
|
return nil, gqlutils.InternalServerError(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if count != 1 {
|
||||||
|
if count == 0 {
|
||||||
|
graphql.AddError(ctx, graphql.ErrorOnPath(
|
||||||
|
ctx,
|
||||||
|
fmt.Errorf("no SAML configuration for email"),
|
||||||
|
))
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
graphql.AddError(
|
||||||
|
ctx,
|
||||||
|
graphql.ErrorOnPath(
|
||||||
|
ctx,
|
||||||
|
fmt.Errorf("multiple SSO configurations found for this domain. Please use your organization-specific SSO login URL"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
samlConfigs, err := r.iam.AccountService.ListSAMLConfigurationsForEmail(ctx, email)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot list SAML configurations for email", log.Error(err))
|
||||||
|
return nil, gqlutils.InternalServerError(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
samlConfig := samlConfigs[0]
|
||||||
|
loginURL := r.baseURL.WithPath("/api/connect/v1/saml/2.0/" + samlConfig.ID.String()).MustString()
|
||||||
|
return &loginURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestLoginURL is the resolver for the testLoginUrl field.
|
// TestLoginURL is the resolver for the testLoginUrl field.
|
||||||
|
|||||||
Reference in New Issue
Block a user