@@ -1,42 +0,0 @@
|
|||||||
/**
|
|
||||||
* @generated SignedSource<<b6a869ebbc0ded2da70f59b7856b906f>>
|
|
||||||
* @lightSyntaxTransform
|
|
||||||
* @nogrep
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
// @ts-nocheck
|
|
||||||
|
|
||||||
import { ReaderFragment } from 'relay-runtime';
|
|
||||||
import { FragmentRefs } from "relay-runtime";
|
|
||||||
export type sessionIsExpiredFragment$data = {
|
|
||||||
readonly expiresAt: any;
|
|
||||||
readonly " $fragmentType": "sessionIsExpiredFragment";
|
|
||||||
};
|
|
||||||
export type sessionIsExpiredFragment$key = {
|
|
||||||
readonly " $data"?: sessionIsExpiredFragment$data;
|
|
||||||
readonly " $fragmentSpreads": FragmentRefs<"sessionIsExpiredFragment">;
|
|
||||||
};
|
|
||||||
|
|
||||||
const node: ReaderFragment = {
|
|
||||||
"argumentDefinitions": [],
|
|
||||||
"kind": "Fragment",
|
|
||||||
"metadata": null,
|
|
||||||
"name": "sessionIsExpiredFragment",
|
|
||||||
"selections": [
|
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"kind": "ScalarField",
|
|
||||||
"name": "expiresAt",
|
|
||||||
"storageKey": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"type": "Session",
|
|
||||||
"abstractKey": null
|
|
||||||
};
|
|
||||||
|
|
||||||
(node as any).hash = "ef7bc8bbfa6d2573b0df09b5b13b00b2";
|
|
||||||
|
|
||||||
export default node;
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Button, Field, IconChevronLeft, useToast } from "@probo/ui";
|
|
||||||
import type { FormEventHandler } from "react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Link, useSearchParams } from "react-router";
|
|
||||||
|
|
||||||
export default function LoginPage() {
|
|
||||||
const { __ } = useTranslate();
|
|
||||||
const { toast } = useToast();
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
|
|
||||||
const authMethod = searchParams.get("method");
|
|
||||||
let initialMode: "default" | "password" | "sso" = "default";
|
|
||||||
if (authMethod === "password") {
|
|
||||||
initialMode = "password";
|
|
||||||
} else if (authMethod === "sso") {
|
|
||||||
initialMode = "sso";
|
|
||||||
}
|
|
||||||
|
|
||||||
const [mode, setMode] = useState<"default" | "password" | "sso">(initialMode);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [isChecking, setIsChecking] = useState(false);
|
|
||||||
|
|
||||||
const handlePasswordLogin: FormEventHandler<HTMLFormElement> = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const formData = new FormData(e.currentTarget);
|
|
||||||
const emailValue = formData.get("email")?.toString();
|
|
||||||
const passwordValue = formData.get("password")?.toString();
|
|
||||||
|
|
||||||
if (!emailValue || !passwordValue) return;
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch("/connect/login", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ email: emailValue, password: passwordValue }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const error = await res.json();
|
|
||||||
throw new Error(error.message || __("Failed to login"));
|
|
||||||
}
|
|
||||||
|
|
||||||
window.location.href = "/";
|
|
||||||
} catch (e: unknown) {
|
|
||||||
toast({
|
|
||||||
title: __("Error"),
|
|
||||||
description: e instanceof Error ? e.message : __("Failed to login"),
|
|
||||||
variant: "error",
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSSOLogin: FormEventHandler<HTMLFormElement> = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const formData = new FormData(e.currentTarget);
|
|
||||||
const emailValue = formData.get("email")?.toString();
|
|
||||||
|
|
||||||
if (!emailValue) return;
|
|
||||||
|
|
||||||
setIsChecking(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch("/connect/check-sso", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ email: emailValue }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const error = await res.json();
|
|
||||||
throw new Error(
|
|
||||||
error.message || __("SSO not available for this email domain")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
if (data.ssoAvailable && data.samlConfigId) {
|
|
||||||
window.location.href = `/connect/saml/login/${data.samlConfigId}`;
|
|
||||||
} else {
|
|
||||||
throw new Error(__("SSO not available for this email domain"));
|
|
||||||
}
|
|
||||||
} catch (e: unknown) {
|
|
||||||
toast({
|
|
||||||
title: __("Error"),
|
|
||||||
description: e instanceof Error ? e.message : __("Failed to login"),
|
|
||||||
variant: "error",
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsChecking(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBack = () => {
|
|
||||||
setMode("default");
|
|
||||||
};
|
|
||||||
|
|
||||||
if (mode === "default") {
|
|
||||||
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" onClick={() => setMode("password")}>
|
|
||||||
{__("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"
|
|
||||||
onClick={() => setMode("sso")}
|
|
||||||
>
|
|
||||||
{__("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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === "password") {
|
|
||||||
return (
|
|
||||||
<form className="space-y-4" onSubmit={handlePasswordLogin}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleBack}
|
|
||||||
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>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<h1 className="text-center text-2xl font-bold">
|
|
||||||
{__("Login with Email")}
|
|
||||||
</h1>
|
|
||||||
<p className="text-center text-txt-tertiary mt-1 mb-6">
|
|
||||||
{__("Enter your email and password")}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
required
|
|
||||||
placeholder={__("Email")}
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
label={__("Email")}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
required
|
|
||||||
placeholder={__("Password")}
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
label={__("Password")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button className="w-full" disabled={isLoading}>
|
|
||||||
{isLoading ? __("Logging in...") : __("Login")}
|
|
||||||
</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>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form className="space-y-4" onSubmit={handleSSOLogin}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleBack}
|
|
||||||
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>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<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={isChecking}>
|
|
||||||
{isChecking ? __("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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
112
apps/console/src/pages/iam/auth/SignInPage.tsx
Normal file
112
apps/console/src/pages/iam/auth/SignInPage.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { useTranslate } from "@probo/i18n";
|
||||||
|
import { Button, Field, useToast } from "@probo/ui";
|
||||||
|
import type { FormEventHandler } from "react";
|
||||||
|
import { useMutation } from "react-relay";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
import { graphql } from "relay-runtime";
|
||||||
|
import type { SignInPageMutation } from "./__generated__/SignInPageMutation.graphql";
|
||||||
|
|
||||||
|
const signInMutation = graphql`
|
||||||
|
mutation SignInPageMutation($input: SignInInput!) {
|
||||||
|
signIn(input: $input) {
|
||||||
|
session {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// TODO initial screen + SAML login
|
||||||
|
export default function SignInPage() {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [signIn, isSigningIn] = useMutation<SignInPageMutation>(signInMutation);
|
||||||
|
|
||||||
|
const handlePasswordLogin: FormEventHandler<HTMLFormElement> = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(e.currentTarget);
|
||||||
|
const emailValue = formData.get("email")?.toString();
|
||||||
|
const passwordValue = formData.get("password")?.toString();
|
||||||
|
|
||||||
|
if (!emailValue || !passwordValue) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
signIn({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
email: emailValue,
|
||||||
|
password: passwordValue,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
window.location.href = "/";
|
||||||
|
} catch (e: unknown) {
|
||||||
|
toast({
|
||||||
|
title: __("Error"),
|
||||||
|
description: e instanceof Error ? e.message : __("Failed to login"),
|
||||||
|
variant: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className="space-y-4" onSubmit={handlePasswordLogin}>
|
||||||
|
{/* <button
|
||||||
|
type="button"
|
||||||
|
onClick={handleBack}
|
||||||
|
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>
|
||||||
|
</button> */}
|
||||||
|
|
||||||
|
<h1 className="text-center text-2xl font-bold">
|
||||||
|
{__("Login with Email")}
|
||||||
|
</h1>
|
||||||
|
<p className="text-center text-txt-tertiary mt-1 mb-6">
|
||||||
|
{__("Enter your email and password")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
required
|
||||||
|
placeholder={__("Email")}
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
label={__("Email")}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
required
|
||||||
|
placeholder={__("Password")}
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
label={__("Password")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button className="w-full" disabled={isSigningIn}>
|
||||||
|
{isSigningIn ? __("Logging in...") : __("Login")}
|
||||||
|
</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>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
106
apps/console/src/pages/iam/auth/__generated__/SignInPageMutation.graphql.ts
generated
Normal file
106
apps/console/src/pages/iam/auth/__generated__/SignInPageMutation.graphql.ts
generated
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<5b81c905d9cd12c78b5f77efdcc654ea>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type SignInInput = {
|
||||||
|
email: any;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
export type SignInPageMutation$variables = {
|
||||||
|
input: SignInInput;
|
||||||
|
};
|
||||||
|
export type SignInPageMutation$data = {
|
||||||
|
readonly signIn: {
|
||||||
|
readonly session: {
|
||||||
|
readonly id: string;
|
||||||
|
} | null | undefined;
|
||||||
|
} | null | undefined;
|
||||||
|
};
|
||||||
|
export type SignInPageMutation = {
|
||||||
|
response: SignInPageMutation$data;
|
||||||
|
variables: SignInPageMutation$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "input",
|
||||||
|
"variableName": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "SignInPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "signIn",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Session",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "session",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "SignInPageMutation",
|
||||||
|
"selections": (v1/*: any*/),
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "SignInPageMutation",
|
||||||
|
"selections": (v1/*: any*/)
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "8e13b6ebdfd4ca6bde158dff53f596db",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "SignInPageMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation SignInPageMutation(\n $input: SignInInput!\n) {\n signIn(input: $input) {\n session {\n id\n }\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "6424104df106b64a6237d92299239a63";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -83,7 +83,7 @@ const routes = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "login",
|
path: "login",
|
||||||
Component: lazy(() => import("./pages/auth/LoginPage")),
|
Component: lazy(() => import("./pages/iam/auth/SignInPage")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "register",
|
path: "register",
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ var (
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ErrUnauthorized = &gqlerror.Error{
|
ErrUnauthenticated = &gqlerror.Error{
|
||||||
Message: "You are not authorized to access this resource",
|
Message: "You must be authenticated to access this resouce",
|
||||||
Extensions: map[string]any{
|
Extensions: map[string]any{
|
||||||
"code": "UNAUTHORIZED",
|
"code": "UNAUTHENTICATED",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ func SessionDirective(ctx context.Context, obj any, next graphql.Resolver, requi
|
|||||||
case types.SessionRequirementOptional:
|
case types.SessionRequirementOptional:
|
||||||
case types.SessionRequirementPresent:
|
case types.SessionRequirementPresent:
|
||||||
if session == nil {
|
if session == nil {
|
||||||
return nil, ErrUnauthorized
|
return nil, ErrUnauthenticated
|
||||||
}
|
}
|
||||||
case types.SessionRequirementNone:
|
case types.SessionRequirementNone:
|
||||||
if session != nil {
|
if session != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user