mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2025-11-02 07:05:47 +00:00
refactor: split login screen and forms
This commit is contained in:
46
site/src/components/auth/login-forn.tsx
Normal file
46
site/src/components/auth/login-forn.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { TextInput, PasswordInput, Button } from "@mantine/core";
|
||||||
|
import { useForm, zodResolver } from "@mantine/form";
|
||||||
|
import { LoginFormValues, loginSchema } from "../../schemas/login-schema";
|
||||||
|
|
||||||
|
interface LoginFormProps {
|
||||||
|
isLoading: boolean;
|
||||||
|
onSubmit: (values: LoginFormValues) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LoginForm = (props: LoginFormProps) => {
|
||||||
|
const { isLoading, onSubmit } = props;
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
mode: "uncontrolled",
|
||||||
|
initialValues: {
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
validate: zodResolver(loginSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||||
|
<TextInput
|
||||||
|
label="Username"
|
||||||
|
placeholder="user@example.com"
|
||||||
|
required
|
||||||
|
disabled={isLoading}
|
||||||
|
key={form.key("username")}
|
||||||
|
{...form.getInputProps("username")}
|
||||||
|
/>
|
||||||
|
<PasswordInput
|
||||||
|
label="Password"
|
||||||
|
placeholder="password"
|
||||||
|
required
|
||||||
|
mt="md"
|
||||||
|
disabled={isLoading}
|
||||||
|
key={form.key("password")}
|
||||||
|
{...form.getInputProps("password")}
|
||||||
|
/>
|
||||||
|
<Button fullWidth mt="xl" type="submit" loading={isLoading}>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
72
site/src/components/auth/oauth-buttons.tsx
Normal file
72
site/src/components/auth/oauth-buttons.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { Grid, Button } from "@mantine/core";
|
||||||
|
import { GithubIcon } from "../../icons/github";
|
||||||
|
import { GoogleIcon } from "../../icons/google";
|
||||||
|
import { OAuthIcon } from "../../icons/oauth";
|
||||||
|
import { TailscaleIcon } from "../../icons/tailscale";
|
||||||
|
|
||||||
|
interface OAuthButtonsProps {
|
||||||
|
oauthProviders: string[];
|
||||||
|
isLoading: boolean;
|
||||||
|
mutate: (provider: string) => void;
|
||||||
|
genericName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OAuthButtons = (props: OAuthButtonsProps) => {
|
||||||
|
const { oauthProviders, isLoading, genericName, mutate } = props;
|
||||||
|
return (
|
||||||
|
<Grid mb="md" mt="md" align="center" justify="center">
|
||||||
|
{oauthProviders.includes("google") && (
|
||||||
|
<Grid.Col span="content">
|
||||||
|
<Button
|
||||||
|
radius="xl"
|
||||||
|
leftSection={<GoogleIcon style={{ width: 14, height: 14 }} />}
|
||||||
|
variant="default"
|
||||||
|
onClick={() => mutate("google")}
|
||||||
|
loading={isLoading}
|
||||||
|
>
|
||||||
|
Google
|
||||||
|
</Button>
|
||||||
|
</Grid.Col>
|
||||||
|
)}
|
||||||
|
{oauthProviders.includes("github") && (
|
||||||
|
<Grid.Col span="content">
|
||||||
|
<Button
|
||||||
|
radius="xl"
|
||||||
|
leftSection={<GithubIcon style={{ width: 14, height: 14 }} />}
|
||||||
|
variant="default"
|
||||||
|
onClick={() => mutate("github")}
|
||||||
|
loading={isLoading}
|
||||||
|
>
|
||||||
|
Github
|
||||||
|
</Button>
|
||||||
|
</Grid.Col>
|
||||||
|
)}
|
||||||
|
{oauthProviders.includes("tailscale") && (
|
||||||
|
<Grid.Col span="content">
|
||||||
|
<Button
|
||||||
|
radius="xl"
|
||||||
|
leftSection={<TailscaleIcon style={{ width: 14, height: 14 }} />}
|
||||||
|
variant="default"
|
||||||
|
onClick={() => mutate("tailscale")}
|
||||||
|
loading={isLoading}
|
||||||
|
>
|
||||||
|
Tailscale
|
||||||
|
</Button>
|
||||||
|
</Grid.Col>
|
||||||
|
)}
|
||||||
|
{oauthProviders.includes("generic") && (
|
||||||
|
<Grid.Col span="content">
|
||||||
|
<Button
|
||||||
|
radius="xl"
|
||||||
|
leftSection={<OAuthIcon style={{ width: 14, height: 14 }} />}
|
||||||
|
variant="default"
|
||||||
|
onClick={() => mutate("generic")}
|
||||||
|
loading={isLoading}
|
||||||
|
>
|
||||||
|
{genericName}
|
||||||
|
</Button>
|
||||||
|
</Grid.Col>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,33 +1,22 @@
|
|||||||
import {
|
import { Paper, Title, Text, Divider } from "@mantine/core";
|
||||||
Button,
|
|
||||||
Paper,
|
|
||||||
PasswordInput,
|
|
||||||
TextInput,
|
|
||||||
Title,
|
|
||||||
Text,
|
|
||||||
Divider,
|
|
||||||
Grid,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { useForm, zodResolver } from "@mantine/form";
|
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { z } from "zod";
|
|
||||||
import { useUserContext } from "../context/user-context";
|
import { useUserContext } from "../context/user-context";
|
||||||
import { Navigate } from "react-router";
|
import { Navigate } from "react-router";
|
||||||
import { Layout } from "../components/layouts/layout";
|
import { Layout } from "../components/layouts/layout";
|
||||||
import { GoogleIcon } from "../icons/google";
|
|
||||||
import { GithubIcon } from "../icons/github";
|
|
||||||
import { OAuthIcon } from "../icons/oauth";
|
|
||||||
import { TailscaleIcon } from "../icons/tailscale";
|
|
||||||
import { isQueryValid } from "../utils/utils";
|
import { isQueryValid } from "../utils/utils";
|
||||||
|
import { OAuthButtons } from "../components/auth/oauth-buttons";
|
||||||
|
import { LoginFormValues } from "../schemas/login-schema";
|
||||||
|
import { LoginForm } from "../components/auth/login-forn";
|
||||||
|
|
||||||
export const LoginPage = () => {
|
export const LoginPage = () => {
|
||||||
const queryString = window.location.search;
|
const queryString = window.location.search;
|
||||||
const params = new URLSearchParams(queryString);
|
const params = new URLSearchParams(queryString);
|
||||||
const redirectUri = params.get("redirect_uri") ?? "";
|
const redirectUri = params.get("redirect_uri") ?? "";
|
||||||
|
|
||||||
const { isLoggedIn, configuredProviders, title, genericName } = useUserContext();
|
const { isLoggedIn, configuredProviders, title, genericName } =
|
||||||
|
useUserContext();
|
||||||
|
|
||||||
const oauthProviders = configuredProviders.filter(
|
const oauthProviders = configuredProviders.filter(
|
||||||
(value) => value !== "username",
|
(value) => value !== "username",
|
||||||
@@ -37,24 +26,8 @@ export const LoginPage = () => {
|
|||||||
return <Navigate to="/logout" />;
|
return <Navigate to="/logout" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema = z.object({
|
|
||||||
username: z.string(),
|
|
||||||
password: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type FormValues = z.infer<typeof schema>;
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
mode: "uncontrolled",
|
|
||||||
initialValues: {
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
},
|
|
||||||
validate: zodResolver(schema),
|
|
||||||
});
|
|
||||||
|
|
||||||
const loginMutation = useMutation({
|
const loginMutation = useMutation({
|
||||||
mutationFn: (login: FormValues) => {
|
mutationFn: (login: LoginFormValues) => {
|
||||||
return axios.post("/api/login", login);
|
return axios.post("/api/login", login);
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
@@ -105,7 +78,7 @@ export const LoginPage = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = (values: FormValues) => {
|
const handleSubmit = (values: LoginFormValues) => {
|
||||||
loginMutation.mutate(values);
|
loginMutation.mutate(values);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,68 +91,12 @@ export const LoginPage = () => {
|
|||||||
<Text size="lg" fw={500} ta="center">
|
<Text size="lg" fw={500} ta="center">
|
||||||
Welcome back, login with
|
Welcome back, login with
|
||||||
</Text>
|
</Text>
|
||||||
<Grid mb="md" mt="md" align="center" justify="center">
|
<OAuthButtons
|
||||||
{oauthProviders.includes("google") && (
|
oauthProviders={oauthProviders}
|
||||||
<Grid.Col span="content">
|
isLoading={loginOAuthMutation.isLoading}
|
||||||
<Button
|
mutate={loginOAuthMutation.mutate}
|
||||||
radius="xl"
|
genericName={genericName}
|
||||||
leftSection={
|
/>
|
||||||
<GoogleIcon style={{ width: 14, height: 14 }} />
|
|
||||||
}
|
|
||||||
variant="default"
|
|
||||||
onClick={() => loginOAuthMutation.mutate("google")}
|
|
||||||
loading={loginOAuthMutation.isLoading}
|
|
||||||
>
|
|
||||||
Google
|
|
||||||
</Button>
|
|
||||||
</Grid.Col>
|
|
||||||
)}
|
|
||||||
{oauthProviders.includes("github") && (
|
|
||||||
<Grid.Col span="content">
|
|
||||||
<Button
|
|
||||||
radius="xl"
|
|
||||||
leftSection={
|
|
||||||
<GithubIcon style={{ width: 14, height: 14 }} />
|
|
||||||
}
|
|
||||||
variant="default"
|
|
||||||
onClick={() => loginOAuthMutation.mutate("github")}
|
|
||||||
loading={loginOAuthMutation.isLoading}
|
|
||||||
>
|
|
||||||
Github
|
|
||||||
</Button>
|
|
||||||
</Grid.Col>
|
|
||||||
)}
|
|
||||||
{oauthProviders.includes("tailscale") && (
|
|
||||||
<Grid.Col span="content">
|
|
||||||
<Button
|
|
||||||
radius="xl"
|
|
||||||
leftSection={
|
|
||||||
<TailscaleIcon style={{ width: 14, height: 14 }} />
|
|
||||||
}
|
|
||||||
variant="default"
|
|
||||||
onClick={() => loginOAuthMutation.mutate("tailscale")}
|
|
||||||
loading={loginOAuthMutation.isLoading}
|
|
||||||
>
|
|
||||||
Tailscale
|
|
||||||
</Button>
|
|
||||||
</Grid.Col>
|
|
||||||
)}
|
|
||||||
{oauthProviders.includes("generic") && (
|
|
||||||
<Grid.Col span="content">
|
|
||||||
<Button
|
|
||||||
radius="xl"
|
|
||||||
leftSection={
|
|
||||||
<OAuthIcon style={{ width: 14, height: 14 }} />
|
|
||||||
}
|
|
||||||
variant="default"
|
|
||||||
onClick={() => loginOAuthMutation.mutate("generic")}
|
|
||||||
loading={loginOAuthMutation.isLoading}
|
|
||||||
>
|
|
||||||
{genericName}
|
|
||||||
</Button>
|
|
||||||
</Grid.Col>
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
{configuredProviders.includes("username") && (
|
{configuredProviders.includes("username") && (
|
||||||
<Divider
|
<Divider
|
||||||
label="Or continue with password"
|
label="Or continue with password"
|
||||||
@@ -190,33 +107,10 @@ export const LoginPage = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{configuredProviders.includes("username") && (
|
{configuredProviders.includes("username") && (
|
||||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
<LoginForm
|
||||||
<TextInput
|
isLoading={loginMutation.isLoading}
|
||||||
label="Username"
|
onSubmit={handleSubmit}
|
||||||
placeholder="user@example.com"
|
/>
|
||||||
required
|
|
||||||
disabled={loginMutation.isLoading}
|
|
||||||
key={form.key("username")}
|
|
||||||
{...form.getInputProps("username")}
|
|
||||||
/>
|
|
||||||
<PasswordInput
|
|
||||||
label="Password"
|
|
||||||
placeholder="password"
|
|
||||||
required
|
|
||||||
mt="md"
|
|
||||||
disabled={loginMutation.isLoading}
|
|
||||||
key={form.key("password")}
|
|
||||||
{...form.getInputProps("password")}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
mt="xl"
|
|
||||||
type="submit"
|
|
||||||
loading={loginMutation.isLoading}
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
8
site/src/schemas/login-schema.ts
Normal file
8
site/src/schemas/login-schema.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const loginSchema = z.object({
|
||||||
|
username: z.string(),
|
||||||
|
password: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type LoginFormValues = z.infer<typeof loginSchema>;
|
||||||
Reference in New Issue
Block a user