mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-11-04 08:05:42 +00:00 
			
		
		
		
	feat: finalize username login
This commit is contained in:
		@@ -19,12 +19,14 @@
 | 
				
			|||||||
        "i18next-resources-to-backend": "^1.2.1",
 | 
					        "i18next-resources-to-backend": "^1.2.1",
 | 
				
			||||||
        "input-otp": "^1.4.2",
 | 
					        "input-otp": "^1.4.2",
 | 
				
			||||||
        "lucide-react": "^0.503.0",
 | 
					        "lucide-react": "^0.503.0",
 | 
				
			||||||
 | 
					        "next-themes": "^0.4.6",
 | 
				
			||||||
        "react": "^19.0.0",
 | 
					        "react": "^19.0.0",
 | 
				
			||||||
        "react-dom": "^19.0.0",
 | 
					        "react-dom": "^19.0.0",
 | 
				
			||||||
        "react-hook-form": "^7.56.3",
 | 
					        "react-hook-form": "^7.56.3",
 | 
				
			||||||
        "react-i18next": "^15.5.1",
 | 
					        "react-i18next": "^15.5.1",
 | 
				
			||||||
        "react-markdown": "^10.1.0",
 | 
					        "react-markdown": "^10.1.0",
 | 
				
			||||||
        "react-router": "^7.5.3",
 | 
					        "react-router": "^7.5.3",
 | 
				
			||||||
 | 
					        "sonner": "^2.0.3",
 | 
				
			||||||
        "tailwind-merge": "^3.2.0",
 | 
					        "tailwind-merge": "^3.2.0",
 | 
				
			||||||
        "tailwindcss": "^4.1.4",
 | 
					        "tailwindcss": "^4.1.4",
 | 
				
			||||||
        "zod": "^3.24.4",
 | 
					        "zod": "^3.24.4",
 | 
				
			||||||
@@ -785,6 +787,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
 | 
					    "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
 | 
					    "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
 | 
					    "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
 | 
				
			||||||
@@ -903,6 +907,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
 | 
					    "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    "sonner": ["sonner@2.0.3", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA=="],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
 | 
					    "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
 | 
					    "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,12 +25,14 @@
 | 
				
			|||||||
    "i18next-resources-to-backend": "^1.2.1",
 | 
					    "i18next-resources-to-backend": "^1.2.1",
 | 
				
			||||||
    "input-otp": "^1.4.2",
 | 
					    "input-otp": "^1.4.2",
 | 
				
			||||||
    "lucide-react": "^0.503.0",
 | 
					    "lucide-react": "^0.503.0",
 | 
				
			||||||
 | 
					    "next-themes": "^0.4.6",
 | 
				
			||||||
    "react": "^19.0.0",
 | 
					    "react": "^19.0.0",
 | 
				
			||||||
    "react-dom": "^19.0.0",
 | 
					    "react-dom": "^19.0.0",
 | 
				
			||||||
    "react-hook-form": "^7.56.3",
 | 
					    "react-hook-form": "^7.56.3",
 | 
				
			||||||
    "react-i18next": "^15.5.1",
 | 
					    "react-i18next": "^15.5.1",
 | 
				
			||||||
    "react-markdown": "^10.1.0",
 | 
					    "react-markdown": "^10.1.0",
 | 
				
			||||||
    "react-router": "^7.5.3",
 | 
					    "react-router": "^7.5.3",
 | 
				
			||||||
 | 
					    "sonner": "^2.0.3",
 | 
				
			||||||
    "tailwind-merge": "^3.2.0",
 | 
					    "tailwind-merge": "^3.2.0",
 | 
				
			||||||
    "tailwindcss": "^4.1.4",
 | 
					    "tailwindcss": "^4.1.4",
 | 
				
			||||||
    "zod": "^3.24.4"
 | 
					    "zod": "^3.24.4"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,5 @@
 | 
				
			|||||||
import { useTranslation } from "react-i18next";
 | 
					import { useTranslation } from "react-i18next";
 | 
				
			||||||
import { Input } from "../ui/input";
 | 
					import { Input } from "../ui/input";
 | 
				
			||||||
import { z } from "zod";
 | 
					 | 
				
			||||||
import { useForm } from "react-hook-form";
 | 
					import { useForm } from "react-hook-form";
 | 
				
			||||||
import { zodResolver } from "@hookform/resolvers/zod";
 | 
					import { zodResolver } from "@hookform/resolvers/zod";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
@@ -12,26 +11,21 @@ import {
 | 
				
			|||||||
  FormMessage,
 | 
					  FormMessage,
 | 
				
			||||||
} from "../ui/form";
 | 
					} from "../ui/form";
 | 
				
			||||||
import { Button } from "../ui/button";
 | 
					import { Button } from "../ui/button";
 | 
				
			||||||
 | 
					import { loginSchema, LoginSchema } from "@/schemas/login-schema";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const LoginForm = () => {
 | 
					interface Props {
 | 
				
			||||||
 | 
					  onSubmit: (data: LoginSchema) => void;
 | 
				
			||||||
 | 
					  loading?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const LoginForm = (props: Props) => {
 | 
				
			||||||
 | 
					  const { onSubmit, loading } = props;
 | 
				
			||||||
  const { t } = useTranslation();
 | 
					  const { t } = useTranslation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const schema = z.object({
 | 
					  const form = useForm<LoginSchema>({
 | 
				
			||||||
    username: z.string(),
 | 
					    resolver: zodResolver(loginSchema),
 | 
				
			||||||
    password: z.string(),
 | 
					 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  type LoginFormType = z.infer<typeof schema>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const form = useForm<LoginFormType>({
 | 
					 | 
				
			||||||
    resolver: zodResolver(schema),
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const onSubmit = (data: LoginFormType) => {
 | 
					 | 
				
			||||||
    // Handle login logic here
 | 
					 | 
				
			||||||
    console.log("Login data:", data);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Form {...form}>
 | 
					    <Form {...form}>
 | 
				
			||||||
      <form onSubmit={form.handleSubmit(onSubmit)}>
 | 
					      <form onSubmit={form.handleSubmit(onSubmit)}>
 | 
				
			||||||
@@ -42,7 +36,11 @@ export const LoginForm = () => {
 | 
				
			|||||||
            <FormItem className="mb-4">
 | 
					            <FormItem className="mb-4">
 | 
				
			||||||
              <FormLabel>{t("loginUsername")}</FormLabel>
 | 
					              <FormLabel>{t("loginUsername")}</FormLabel>
 | 
				
			||||||
              <FormControl>
 | 
					              <FormControl>
 | 
				
			||||||
                <Input placeholder={t("loginUsername")} {...field} />
 | 
					                <Input
 | 
				
			||||||
 | 
					                  placeholder={t("loginUsername")}
 | 
				
			||||||
 | 
					                  disabled={loading}
 | 
				
			||||||
 | 
					                  {...field}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
              </FormControl>
 | 
					              </FormControl>
 | 
				
			||||||
              <FormMessage />
 | 
					              <FormMessage />
 | 
				
			||||||
            </FormItem>
 | 
					            </FormItem>
 | 
				
			||||||
@@ -66,6 +64,7 @@ export const LoginForm = () => {
 | 
				
			|||||||
                <Input
 | 
					                <Input
 | 
				
			||||||
                  placeholder={t("loginPassword")}
 | 
					                  placeholder={t("loginPassword")}
 | 
				
			||||||
                  type="password"
 | 
					                  type="password"
 | 
				
			||||||
 | 
					                  disabled={loading}
 | 
				
			||||||
                  {...field}
 | 
					                  {...field}
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
              </FormControl>
 | 
					              </FormControl>
 | 
				
			||||||
@@ -73,7 +72,7 @@ export const LoginForm = () => {
 | 
				
			|||||||
            </FormItem>
 | 
					            </FormItem>
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <Button className="w-full" type="submit">
 | 
					        <Button className="w-full" type="submit" loading={loading}>
 | 
				
			||||||
          {t("loginSubmit")}
 | 
					          {t("loginSubmit")}
 | 
				
			||||||
        </Button>
 | 
					        </Button>
 | 
				
			||||||
      </form>
 | 
					      </form>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,3 @@
 | 
				
			|||||||
import { z } from "zod";
 | 
					 | 
				
			||||||
import { Form, FormControl, FormField, FormItem } from "../ui/form";
 | 
					import { Form, FormControl, FormField, FormItem } from "../ui/form";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  InputOTP,
 | 
					  InputOTP,
 | 
				
			||||||
@@ -8,23 +7,19 @@ import {
 | 
				
			|||||||
} from "../ui/input-otp";
 | 
					} from "../ui/input-otp";
 | 
				
			||||||
import { zodResolver } from "@hookform/resolvers/zod";
 | 
					import { zodResolver } from "@hookform/resolvers/zod";
 | 
				
			||||||
import { useForm } from "react-hook-form";
 | 
					import { useForm } from "react-hook-form";
 | 
				
			||||||
 | 
					import { totpSchema, TotpSchema } from "@/schemas/totp-schema";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Props {
 | 
					interface Props {
 | 
				
			||||||
  formId: string;
 | 
					  formId: string;
 | 
				
			||||||
  onSubmit: (code: FormValues) => void;
 | 
					  onSubmit: (code: TotpSchema) => void;
 | 
				
			||||||
 | 
					  loading?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const schema = z.object({
 | 
					 | 
				
			||||||
  code: z.string(),
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export type FormValues = z.infer<typeof schema>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const TotpForm = (props: Props) => {
 | 
					export const TotpForm = (props: Props) => {
 | 
				
			||||||
  const { formId, onSubmit } = props;
 | 
					  const { formId, onSubmit, loading } = props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const form = useForm<FormValues>({
 | 
					  const form = useForm<TotpSchema>({
 | 
				
			||||||
    resolver: zodResolver(schema),
 | 
					    resolver: zodResolver(totpSchema),
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
@@ -36,7 +31,7 @@ export const TotpForm = (props: Props) => {
 | 
				
			|||||||
          render={({ field }) => (
 | 
					          render={({ field }) => (
 | 
				
			||||||
            <FormItem>
 | 
					            <FormItem>
 | 
				
			||||||
              <FormControl>
 | 
					              <FormControl>
 | 
				
			||||||
                <InputOTP maxLength={6} {...field}>
 | 
					                <InputOTP maxLength={6} disabled={loading} {...field}>
 | 
				
			||||||
                  <InputOTPGroup>
 | 
					                  <InputOTPGroup>
 | 
				
			||||||
                    <InputOTPSlot index={0} />
 | 
					                    <InputOTPSlot index={0} />
 | 
				
			||||||
                    <InputOTPSlot index={1} />
 | 
					                    <InputOTPSlot index={1} />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@ import { Slot } from "@radix-ui/react-slot";
 | 
				
			|||||||
import { cva, type VariantProps } from "class-variance-authority";
 | 
					import { cva, type VariantProps } from "class-variance-authority";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { cn } from "@/lib/utils";
 | 
					import { cn } from "@/lib/utils";
 | 
				
			||||||
 | 
					import { Loader2 } from "lucide-react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const buttonVariants = cva(
 | 
					const buttonVariants = cva(
 | 
				
			||||||
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
 | 
					  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
 | 
				
			||||||
@@ -42,13 +43,28 @@ function Button({
 | 
				
			|||||||
  variant,
 | 
					  variant,
 | 
				
			||||||
  size,
 | 
					  size,
 | 
				
			||||||
  asChild = false,
 | 
					  asChild = false,
 | 
				
			||||||
 | 
					  loading = false,
 | 
				
			||||||
  ...props
 | 
					  ...props
 | 
				
			||||||
}: React.ComponentProps<"button"> &
 | 
					}: React.ComponentProps<"button"> &
 | 
				
			||||||
  VariantProps<typeof buttonVariants> & {
 | 
					  VariantProps<typeof buttonVariants> & {
 | 
				
			||||||
    asChild?: boolean;
 | 
					    asChild?: boolean;
 | 
				
			||||||
 | 
					    loading?: boolean;
 | 
				
			||||||
  }) {
 | 
					  }) {
 | 
				
			||||||
  const Comp = asChild ? Slot : "button";
 | 
					  const Comp = asChild ? Slot : "button";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (loading) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <Comp
 | 
				
			||||||
 | 
					        data-slot="button"
 | 
				
			||||||
 | 
					        className={cn(buttonVariants({ variant, size, className }))}
 | 
				
			||||||
 | 
					        disabled
 | 
				
			||||||
 | 
					        {...props}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Loader2 className="animate-spin" />
 | 
				
			||||||
 | 
					      </Comp>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Comp
 | 
					    <Comp
 | 
				
			||||||
      data-slot="button"
 | 
					      data-slot="button"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										23
									
								
								frontend/src/components/ui/sonner.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								frontend/src/components/ui/sonner.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					import { useTheme } from "next-themes"
 | 
				
			||||||
 | 
					import { Toaster as Sonner, ToasterProps } from "sonner"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Toaster = ({ ...props }: ToasterProps) => {
 | 
				
			||||||
 | 
					  const { theme = "system" } = useTheme()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Sonner
 | 
				
			||||||
 | 
					      theme={theme as ToasterProps["theme"]}
 | 
				
			||||||
 | 
					      className="toaster group"
 | 
				
			||||||
 | 
					      style={
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "--normal-bg": "var(--popover)",
 | 
				
			||||||
 | 
					          "--normal-text": "var(--popover-foreground)",
 | 
				
			||||||
 | 
					          "--normal-border": "var(--border)",
 | 
				
			||||||
 | 
					        } as React.CSSProperties
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Toaster }
 | 
				
			||||||
@@ -15,6 +15,7 @@ import { UnauthorizedPage } from "./pages/unauthorized-page.tsx";
 | 
				
			|||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 | 
					import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 | 
				
			||||||
import { AppContextProvider } from "./context/app-context.tsx";
 | 
					import { AppContextProvider } from "./context/app-context.tsx";
 | 
				
			||||||
import { UserContextProvider } from "./context/user-context.tsx";
 | 
					import { UserContextProvider } from "./context/user-context.tsx";
 | 
				
			||||||
 | 
					import { Toaster } from "@/components/ui/sonner";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const router = createBrowserRouter([
 | 
					const router = createBrowserRouter([
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
@@ -52,6 +53,11 @@ const router = createBrowserRouter([
 | 
				
			|||||||
    element: <UnauthorizedPage />,
 | 
					    element: <UnauthorizedPage />,
 | 
				
			||||||
    errorElement: <ErrorPage />,
 | 
					    errorElement: <ErrorPage />,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: "/error",
 | 
				
			||||||
 | 
					    element: <ErrorPage />,
 | 
				
			||||||
 | 
					    errorElement: <ErrorPage />,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    path: "*",
 | 
					    path: "*",
 | 
				
			||||||
    element: <NotFoundPage />,
 | 
					    element: <NotFoundPage />,
 | 
				
			||||||
@@ -68,6 +74,7 @@ createRoot(document.getElementById("root")!).render(
 | 
				
			|||||||
        <UserContextProvider>
 | 
					        <UserContextProvider>
 | 
				
			||||||
          <Layout>
 | 
					          <Layout>
 | 
				
			||||||
            <RouterProvider router={router} />
 | 
					            <RouterProvider router={router} />
 | 
				
			||||||
 | 
					            <Toaster />
 | 
				
			||||||
          </Layout>
 | 
					          </Layout>
 | 
				
			||||||
        </UserContextProvider>
 | 
					        </UserContextProvider>
 | 
				
			||||||
      </AppContextProvider>
 | 
					      </AppContextProvider>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,24 +20,24 @@ export const ContinuePage = () => {
 | 
				
			|||||||
  const { domain, disableContinue } = useAppContext();
 | 
					  const { domain, disableContinue } = useAppContext();
 | 
				
			||||||
  const { t } = useTranslation();
 | 
					  const { t } = useTranslation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const navigate = useNavigate();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!isLoggedIn) {
 | 
					  if (!isLoggedIn) {
 | 
				
			||||||
    return <Navigate to="/login" />;
 | 
					    return <Navigate to="/login" />;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!redirectURI) {
 | 
					  if (!redirectURI) {
 | 
				
			||||||
    return <Navigate to="/" />;
 | 
					    return <Navigate to="/logout" />;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!isValidUrl(redirectURI)) {
 | 
					  if (!isValidUrl(redirectURI)) {
 | 
				
			||||||
    return <Navigate to="/" />;
 | 
					    return <Navigate to="/logout" />;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (disableContinue) {
 | 
					  if (disableContinue) {
 | 
				
			||||||
    window.location.href = redirectURI;
 | 
					    window.location.href = redirectURI;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const url = new URL(redirectURI);
 | 
					  const url = new URL(redirectURI);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!url.hostname.includes(domain)) {
 | 
					  if (!url.hostname.includes(domain)) {
 | 
				
			||||||
@@ -60,12 +60,12 @@ export const ContinuePage = () => {
 | 
				
			|||||||
        </CardHeader>
 | 
					        </CardHeader>
 | 
				
			||||||
        <CardFooter className="flex flex-col items-stretch gap-2">
 | 
					        <CardFooter className="flex flex-col items-stretch gap-2">
 | 
				
			||||||
          <Button
 | 
					          <Button
 | 
				
			||||||
            onClick={() => window.location.replace(redirectURI)}
 | 
					            onClick={() => (window.location.href = redirectURI)}
 | 
				
			||||||
            variant="destructive"
 | 
					            variant="destructive"
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            {t("continueTitle")}
 | 
					            {t("continueTitle")}
 | 
				
			||||||
          </Button>
 | 
					          </Button>
 | 
				
			||||||
          <Button onClick={() => navigate("/")} variant="outline">
 | 
					          <Button onClick={() => navigate("/logout")} variant="outline">
 | 
				
			||||||
            {t("cancelTitle")}
 | 
					            {t("cancelTitle")}
 | 
				
			||||||
          </Button>
 | 
					          </Button>
 | 
				
			||||||
        </CardFooter>
 | 
					        </CardFooter>
 | 
				
			||||||
@@ -92,12 +92,12 @@ export const ContinuePage = () => {
 | 
				
			|||||||
        </CardHeader>
 | 
					        </CardHeader>
 | 
				
			||||||
        <CardFooter className="flex flex-col items-stretch gap-2">
 | 
					        <CardFooter className="flex flex-col items-stretch gap-2">
 | 
				
			||||||
          <Button
 | 
					          <Button
 | 
				
			||||||
            onClick={() => window.location.replace(redirectURI)}
 | 
					            onClick={() => (window.location.href = redirectURI)}
 | 
				
			||||||
            variant="warning"
 | 
					            variant="warning"
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            {t("continueTitle")}
 | 
					            {t("continueTitle")}
 | 
				
			||||||
          </Button>
 | 
					          </Button>
 | 
				
			||||||
          <Button onClick={() => navigate("/")} variant="outline">
 | 
					          <Button onClick={() => navigate("/logout")} variant="outline">
 | 
				
			||||||
            {t("cancelTitle")}
 | 
					            {t("cancelTitle")}
 | 
				
			||||||
          </Button>
 | 
					          </Button>
 | 
				
			||||||
        </CardFooter>
 | 
					        </CardFooter>
 | 
				
			||||||
@@ -112,7 +112,7 @@ export const ContinuePage = () => {
 | 
				
			|||||||
        <CardDescription>{t("continueSubtitle")}</CardDescription>
 | 
					        <CardDescription>{t("continueSubtitle")}</CardDescription>
 | 
				
			||||||
      </CardHeader>
 | 
					      </CardHeader>
 | 
				
			||||||
      <CardFooter className="flex flex-col items-stretch">
 | 
					      <CardFooter className="flex flex-col items-stretch">
 | 
				
			||||||
        <Button onClick={() => window.location.replace(redirectURI)}>
 | 
					        <Button onClick={() => (window.location.href = redirectURI)}>
 | 
				
			||||||
          {t("continueTitle")}
 | 
					          {t("continueTitle")}
 | 
				
			||||||
        </Button>
 | 
					        </Button>
 | 
				
			||||||
      </CardFooter>
 | 
					      </CardFooter>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,19 +12,57 @@ import {
 | 
				
			|||||||
import { OAuthButton } from "@/components/ui/oauth-button";
 | 
					import { OAuthButton } from "@/components/ui/oauth-button";
 | 
				
			||||||
import { SeperatorWithChildren } from "@/components/ui/separator";
 | 
					import { SeperatorWithChildren } from "@/components/ui/separator";
 | 
				
			||||||
import { useAppContext } from "@/context/app-context";
 | 
					import { useAppContext } from "@/context/app-context";
 | 
				
			||||||
 | 
					import { useUserContext } from "@/context/user-context";
 | 
				
			||||||
 | 
					import { LoginSchema } from "@/schemas/login-schema";
 | 
				
			||||||
 | 
					import { useMutation } from "@tanstack/react-query";
 | 
				
			||||||
 | 
					import axios from "axios";
 | 
				
			||||||
import { useTranslation } from "react-i18next";
 | 
					import { useTranslation } from "react-i18next";
 | 
				
			||||||
 | 
					import { Navigate } from "react-router";
 | 
				
			||||||
 | 
					import { toast } from "sonner";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const LoginPage = () => {
 | 
					export const LoginPage = () => {
 | 
				
			||||||
  const { configuredProviders, title } = useAppContext();
 | 
					  const searchParams = new URLSearchParams(window.location.search);
 | 
				
			||||||
 | 
					  const redirectUri = searchParams.get("redirect_uri");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  console.log("Configured providers:", configuredProviders);
 | 
					  const { isLoggedIn } = useUserContext();
 | 
				
			||||||
 | 
					  const { configuredProviders, title } = useAppContext();
 | 
				
			||||||
  const { t } = useTranslation();
 | 
					  const { t } = useTranslation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (isLoggedIn) {
 | 
				
			||||||
 | 
					    return <Navigate to="/logout" />;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const oauthConfigured =
 | 
					  const oauthConfigured =
 | 
				
			||||||
    configuredProviders.filter((provider) => provider !== "username").length >
 | 
					    configuredProviders.filter((provider) => provider !== "username").length >
 | 
				
			||||||
    0;
 | 
					    0;
 | 
				
			||||||
  const userAuthConfigured = configuredProviders.includes("username");
 | 
					  const userAuthConfigured = configuredProviders.includes("username");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const loginMutation = useMutation({
 | 
				
			||||||
 | 
					    mutationFn: (values: LoginSchema) => axios.post("/api/login", values),
 | 
				
			||||||
 | 
					    mutationKey: ["login"],
 | 
				
			||||||
 | 
					    onSuccess: (data) => {
 | 
				
			||||||
 | 
					      if (data.data.totpPending) {
 | 
				
			||||||
 | 
					        window.location.replace(`/totp?redirect_uri=${redirectUri}`);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      toast.success(t("loginSuccessTitle"), {
 | 
				
			||||||
 | 
					        description: t("loginSuccessSubtitle"),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      setTimeout(() => {
 | 
				
			||||||
 | 
					        window.location.replace(`/continue?redirect_uri=${redirectUri}`);
 | 
				
			||||||
 | 
					      }, 500);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    onError: (error: Error) => {
 | 
				
			||||||
 | 
					      toast.error(t("loginFailTitle"), {
 | 
				
			||||||
 | 
					        description: error.message.includes("429")
 | 
				
			||||||
 | 
					          ? t("loginFailRateLimit")
 | 
				
			||||||
 | 
					          : t("loginFailSubtitle"),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Card className="min-w-xs sm:min-w-sm">
 | 
					    <Card className="min-w-xs sm:min-w-sm">
 | 
				
			||||||
      <CardHeader>
 | 
					      <CardHeader>
 | 
				
			||||||
@@ -64,7 +102,12 @@ export const LoginPage = () => {
 | 
				
			|||||||
        {userAuthConfigured && oauthConfigured && (
 | 
					        {userAuthConfigured && oauthConfigured && (
 | 
				
			||||||
          <SeperatorWithChildren>{t("loginDivider")}</SeperatorWithChildren>
 | 
					          <SeperatorWithChildren>{t("loginDivider")}</SeperatorWithChildren>
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
        {userAuthConfigured && <LoginForm />}
 | 
					        {userAuthConfigured && (
 | 
				
			||||||
 | 
					          <LoginForm
 | 
				
			||||||
 | 
					            onSubmit={(values) => loginMutation.mutate(values)}
 | 
				
			||||||
 | 
					            loading={loginMutation.isPending}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
        {configuredProviders.length == 0 && (
 | 
					        {configuredProviders.length == 0 && (
 | 
				
			||||||
          <h3 className="text-center text-xl text-red-600">
 | 
					          <h3 className="text-center text-xl text-red-600">
 | 
				
			||||||
            {t("failedToFetchProvidersTitle")}
 | 
					            {t("failedToFetchProvidersTitle")}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,8 +9,11 @@ import {
 | 
				
			|||||||
import { useAppContext } from "@/context/app-context";
 | 
					import { useAppContext } from "@/context/app-context";
 | 
				
			||||||
import { useUserContext } from "@/context/user-context";
 | 
					import { useUserContext } from "@/context/user-context";
 | 
				
			||||||
import { capitalize } from "@/lib/utils";
 | 
					import { capitalize } from "@/lib/utils";
 | 
				
			||||||
 | 
					import { useMutation } from "@tanstack/react-query";
 | 
				
			||||||
 | 
					import axios from "axios";
 | 
				
			||||||
import { Trans, useTranslation } from "react-i18next";
 | 
					import { Trans, useTranslation } from "react-i18next";
 | 
				
			||||||
import { Navigate } from "react-router";
 | 
					import { Navigate } from "react-router";
 | 
				
			||||||
 | 
					import { toast } from "sonner";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const LogoutPage = () => {
 | 
					export const LogoutPage = () => {
 | 
				
			||||||
  const { provider, username, email, isLoggedIn } = useUserContext();
 | 
					  const { provider, username, email, isLoggedIn } = useUserContext();
 | 
				
			||||||
@@ -21,6 +24,25 @@ export const LogoutPage = () => {
 | 
				
			|||||||
    return <Navigate to="/login" />;
 | 
					    return <Navigate to="/login" />;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const logoutMutation = useMutation({
 | 
				
			||||||
 | 
					    mutationFn: () => axios.post("/api/logout"),
 | 
				
			||||||
 | 
					    mutationKey: ["logout"],
 | 
				
			||||||
 | 
					    onSuccess: () => {
 | 
				
			||||||
 | 
					      toast.success(t("logoutSuccessTitle"), {
 | 
				
			||||||
 | 
					        description: t("logoutSuccessSubtitle"),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      setTimeout(async () => {
 | 
				
			||||||
 | 
					        window.location.replace("/login");
 | 
				
			||||||
 | 
					      }, 500);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    onError: () => {
 | 
				
			||||||
 | 
					      toast.error(t("logoutFailTitle"), {
 | 
				
			||||||
 | 
					        description: t("logoutFailSubtitle"),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Card className="min-w-xs sm:min-w-sm">
 | 
					    <Card className="min-w-xs sm:min-w-sm">
 | 
				
			||||||
      <CardHeader>
 | 
					      <CardHeader>
 | 
				
			||||||
@@ -47,14 +69,19 @@ export const LogoutPage = () => {
 | 
				
			|||||||
                code: <code />,
 | 
					                code: <code />,
 | 
				
			||||||
              }}
 | 
					              }}
 | 
				
			||||||
              values={{
 | 
					              values={{
 | 
				
			||||||
                username: username,
 | 
					                username,
 | 
				
			||||||
              }}
 | 
					              }}
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
        </CardDescription>
 | 
					        </CardDescription>
 | 
				
			||||||
      </CardHeader>
 | 
					      </CardHeader>
 | 
				
			||||||
      <CardFooter className="flex flex-col items-stretch">
 | 
					      <CardFooter className="flex flex-col items-stretch">
 | 
				
			||||||
        <Button>{t("logoutTitle")}</Button>
 | 
					        <Button
 | 
				
			||||||
 | 
					          loading={logoutMutation.isPending}
 | 
				
			||||||
 | 
					          onClick={() => logoutMutation.mutate()}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {t("logoutTitle")}
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
      </CardFooter>
 | 
					      </CardFooter>
 | 
				
			||||||
    </Card>
 | 
					    </Card>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import { FormValues, TotpForm } from "@/components/auth/totp-form";
 | 
					import { TotpForm } from "@/components/auth/totp-form";
 | 
				
			||||||
import { Button } from "@/components/ui/button";
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Card,
 | 
					  Card,
 | 
				
			||||||
@@ -8,16 +8,40 @@ import {
 | 
				
			|||||||
  CardHeader,
 | 
					  CardHeader,
 | 
				
			||||||
  CardTitle,
 | 
					  CardTitle,
 | 
				
			||||||
} from "@/components/ui/card";
 | 
					} from "@/components/ui/card";
 | 
				
			||||||
 | 
					import { TotpSchema } from "@/schemas/totp-schema";
 | 
				
			||||||
 | 
					import { useMutation } from "@tanstack/react-query";
 | 
				
			||||||
 | 
					import axios from "axios";
 | 
				
			||||||
import { useId } from "react";
 | 
					import { useId } from "react";
 | 
				
			||||||
import { useTranslation } from "react-i18next";
 | 
					import { useTranslation } from "react-i18next";
 | 
				
			||||||
 | 
					import { useNavigate } from "react-router";
 | 
				
			||||||
 | 
					import { toast } from "sonner";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const TotpPage = () => {
 | 
					export const TotpPage = () => {
 | 
				
			||||||
 | 
					  const searchParams = new URLSearchParams(window.location.search);
 | 
				
			||||||
 | 
					  const redirectUri = searchParams.get("redirect_uri");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { t } = useTranslation();
 | 
					  const { t } = useTranslation();
 | 
				
			||||||
  const formId = useId();
 | 
					  const formId = useId();
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const onSubmit = (data: FormValues) => {
 | 
					  const totpMutation = useMutation({
 | 
				
			||||||
    console.log("TOTP data:", data);
 | 
					    mutationFn: (values: TotpSchema) => axios.post("/api/totp", values),
 | 
				
			||||||
  };
 | 
					    mutationKey: ["totp"],
 | 
				
			||||||
 | 
					    onSuccess: () => {
 | 
				
			||||||
 | 
					      toast.success(t("totpSuccessTitle"), {
 | 
				
			||||||
 | 
					        description: t("totpSuccessSubtitle"),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      setTimeout(() => {
 | 
				
			||||||
 | 
					        navigate(`/continue?redirect_uri=${redirectUri}`);
 | 
				
			||||||
 | 
					      }, 500);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    onError: () => {
 | 
				
			||||||
 | 
					      toast.error(t("totpFailTitle"), {
 | 
				
			||||||
 | 
					        description: t("totpFailSubtitle"),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Card className="min-w-xs sm:min-w-sm">
 | 
					    <Card className="min-w-xs sm:min-w-sm">
 | 
				
			||||||
@@ -26,10 +50,14 @@ export const TotpPage = () => {
 | 
				
			|||||||
        <CardDescription>{t("totpSubtitle")}</CardDescription>
 | 
					        <CardDescription>{t("totpSubtitle")}</CardDescription>
 | 
				
			||||||
      </CardHeader>
 | 
					      </CardHeader>
 | 
				
			||||||
      <CardContent className="flex flex-col items-center">
 | 
					      <CardContent className="flex flex-col items-center">
 | 
				
			||||||
        <TotpForm formId={formId} onSubmit={onSubmit} />
 | 
					        <TotpForm
 | 
				
			||||||
 | 
					          formId={formId}
 | 
				
			||||||
 | 
					          onSubmit={(values) => totpMutation.mutate(values)}
 | 
				
			||||||
 | 
					          loading={totpMutation.isPending}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
      </CardContent>
 | 
					      </CardContent>
 | 
				
			||||||
      <CardFooter className="flex flex-col items-stretch">
 | 
					      <CardFooter className="flex flex-col items-stretch">
 | 
				
			||||||
        <Button form={formId} type="submit">
 | 
					        <Button form={formId} type="submit" loading={totpMutation.isPending}>
 | 
				
			||||||
          {t("continueTitle")}
 | 
					          {t("continueTitle")}
 | 
				
			||||||
        </Button>
 | 
					        </Button>
 | 
				
			||||||
      </CardFooter>
 | 
					      </CardFooter>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,12 +16,13 @@ export const UnauthorizedPage = () => {
 | 
				
			|||||||
  const groupErr = searchParams.get("groupErr");
 | 
					  const groupErr = searchParams.get("groupErr");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { t } = useTranslation();
 | 
					  const { t } = useTranslation();
 | 
				
			||||||
  const navigate = useNavigate();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!username) {
 | 
					  if (!username) {
 | 
				
			||||||
    return <Navigate to="/" />;
 | 
					    return <Navigate to="/" />;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let i18nKey = "unaothorizedLoginSubtitle";
 | 
					  let i18nKey = "unaothorizedLoginSubtitle";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (resource) {
 | 
					  if (resource) {
 | 
				
			||||||
@@ -44,8 +45,8 @@ export const UnauthorizedPage = () => {
 | 
				
			|||||||
              code: <code />,
 | 
					              code: <code />,
 | 
				
			||||||
            }}
 | 
					            }}
 | 
				
			||||||
            values={{
 | 
					            values={{
 | 
				
			||||||
              username: username,
 | 
					              username,
 | 
				
			||||||
              resource: resource,
 | 
					              resource,
 | 
				
			||||||
            }}
 | 
					            }}
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        </CardDescription>
 | 
					        </CardDescription>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										8
									
								
								frontend/src/schemas/login-schema.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								frontend/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 LoginSchema = z.infer<typeof loginSchema>;
 | 
				
			||||||
							
								
								
									
										7
									
								
								frontend/src/schemas/totp-schema.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								frontend/src/schemas/totp-schema.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const totpSchema = z.object({
 | 
				
			||||||
 | 
					  code: z.string(),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type TotpSchema = z.infer<typeof totpSchema>;
 | 
				
			||||||
@@ -179,7 +179,7 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// We are using caddy/traefik so redirect
 | 
								// We are using caddy/traefik so redirect
 | 
				
			||||||
			c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", h.Config.AppURL, queries.Encode()))
 | 
								c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/unauthorized?%s", h.Config.AppURL, queries.Encode()))
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -227,7 +227,7 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
 | 
				
			|||||||
	log.Debug().Interface("redirect_uri", fmt.Sprintf("%s://%s%s", proto, host, uri)).Msg("Redirecting to login")
 | 
						log.Debug().Interface("redirect_uri", fmt.Sprintf("%s://%s%s", proto, host, uri)).Msg("Redirecting to login")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Redirect to login
 | 
						// Redirect to login
 | 
				
			||||||
	c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/?%s", h.Config.AppURL, queries.Encode()))
 | 
						c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/login?%s", h.Config.AppURL, queries.Encode()))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (h *Handlers) LoginHandler(c *gin.Context) {
 | 
					func (h *Handlers) LoginHandler(c *gin.Context) {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user