Replug password sign in

Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Émile Ré
2025-12-19 19:35:27 +01:00
committed by Bryan Frimin
parent f72851df4d
commit e4df37b5c8
6 changed files with 223 additions and 313 deletions

View File

@@ -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;

View File

@@ -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>
);
}

View 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>
);
}

View 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;

View File

@@ -83,7 +83,7 @@ const routes = [
children: [
{
path: "login",
Component: lazy(() => import("./pages/auth/LoginPage")),
Component: lazy(() => import("./pages/iam/auth/SignInPage")),
},
{
path: "register",

View File

@@ -36,10 +36,10 @@ var (
},
}
ErrUnauthorized = &gqlerror.Error{
Message: "You are not authorized to access this resource",
ErrUnauthenticated = &gqlerror.Error{
Message: "You must be authenticated to access this resouce",
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.SessionRequirementPresent:
if session == nil {
return nil, ErrUnauthorized
return nil, ErrUnauthenticated
}
case types.SessionRequirementNone:
if session != nil {