ソースを参照

added support for pin code auth

Milo Schwartz 8 ヶ月 前
コミット
ad5ea3564b

+ 49 - 39
server/routers/external.ts

@@ -43,51 +43,51 @@ authenticated.get(
     "/org/:orgId",
     verifyOrgAccess,
     verifyUserHasAction(ActionsEnum.getOrg),
-    org.getOrg
+    org.getOrg,
 );
 authenticated.post(
     "/org/:orgId",
     verifyOrgAccess,
     verifyUserHasAction(ActionsEnum.updateOrg),
-    org.updateOrg
+    org.updateOrg,
 );
 authenticated.delete(
     "/org/:orgId",
     verifyOrgAccess,
     verifyUserIsOrgOwner,
-    org.deleteOrg
+    org.deleteOrg,
 );
 
 authenticated.put(
     "/org/:orgId/site",
     verifyOrgAccess,
     verifyUserHasAction(ActionsEnum.createSite),
-    site.createSite
+    site.createSite,
 );
 authenticated.get(
     "/org/:orgId/sites",
     verifyOrgAccess,
     verifyUserHasAction(ActionsEnum.listSites),
-    site.listSites
+    site.listSites,
 );
 authenticated.get(
     "/org/:orgId/site/:niceId",
     verifyOrgAccess,
     verifyUserHasAction(ActionsEnum.getSite),
-    site.getSite
+    site.getSite,
 );
 
 authenticated.get(
     "/org/:orgId/pick-site-defaults",
     verifyOrgAccess,
     verifyUserHasAction(ActionsEnum.createSite),
-    site.pickSiteDefaults
+    site.pickSiteDefaults,
 );
 authenticated.get(
     "/site/:siteId",
     verifySiteAccess,
     verifyUserHasAction(ActionsEnum.getSite),
-    site.getSite
+    site.getSite,
 );
 // authenticated.get(
 //     "/site/:siteId/roles",
@@ -99,38 +99,38 @@ authenticated.post(
     "/site/:siteId",
     verifySiteAccess,
     verifyUserHasAction(ActionsEnum.updateSite),
-    site.updateSite
+    site.updateSite,
 );
 authenticated.delete(
     "/site/:siteId",
     verifySiteAccess,
     verifyUserHasAction(ActionsEnum.deleteSite),
-    site.deleteSite
+    site.deleteSite,
 );
 
 authenticated.put(
     "/org/:orgId/site/:siteId/resource",
     verifyOrgAccess,
     verifyUserHasAction(ActionsEnum.createResource),
-    resource.createResource
+    resource.createResource,
 );
 authenticated.get(
     "/site/:siteId/resources",
     verifyUserHasAction(ActionsEnum.listResources),
-    resource.listResources
+    resource.listResources,
 );
 authenticated.get(
     "/org/:orgId/resources",
     verifyOrgAccess,
     verifyUserHasAction(ActionsEnum.listResources),
-    resource.listResources
+    resource.listResources,
 );
 
 authenticated.post(
     "/org/:orgId/create-invite",
     verifyOrgAccess,
     verifyUserHasAction(ActionsEnum.inviteUser),
-    user.inviteUser
+    user.inviteUser,
 ); // maybe make this /invite/create instead
 authenticated.post("/invite/accept", user.acceptInvite);
 
@@ -138,77 +138,77 @@ authenticated.get(
     "/resource/:resourceId/roles",
     verifyResourceAccess,
     verifyUserHasAction(ActionsEnum.listResourceRoles),
-    resource.listResourceRoles
+    resource.listResourceRoles,
 );
 
 authenticated.get(
     "/resource/:resourceId/users",
     verifyResourceAccess,
     verifyUserHasAction(ActionsEnum.listResourceUsers),
-    resource.listResourceUsers
+    resource.listResourceUsers,
 );
 
 authenticated.get(
     "/resource/:resourceId",
     verifyResourceAccess,
     verifyUserHasAction(ActionsEnum.getResource),
-    resource.getResource
+    resource.getResource,
 );
 authenticated.post(
     "/resource/:resourceId",
     verifyResourceAccess,
     verifyUserHasAction(ActionsEnum.updateResource),
-    resource.updateResource
+    resource.updateResource,
 );
 authenticated.delete(
     "/resource/:resourceId",
     verifyResourceAccess,
     verifyUserHasAction(ActionsEnum.deleteResource),
-    resource.deleteResource
+    resource.deleteResource,
 );
 
 authenticated.put(
     "/resource/:resourceId/target",
     verifyResourceAccess,
     verifyUserHasAction(ActionsEnum.createTarget),
-    target.createTarget
+    target.createTarget,
 );
 authenticated.get(
     "/resource/:resourceId/targets",
     verifyResourceAccess,
     verifyUserHasAction(ActionsEnum.listTargets),
-    target.listTargets
+    target.listTargets,
 );
 authenticated.get(
     "/target/:targetId",
     verifyTargetAccess,
     verifyUserHasAction(ActionsEnum.getTarget),
-    target.getTarget
+    target.getTarget,
 );
 authenticated.post(
     "/target/:targetId",
     verifyTargetAccess,
     verifyUserHasAction(ActionsEnum.updateTarget),
-    target.updateTarget
+    target.updateTarget,
 );
 authenticated.delete(
     "/target/:targetId",
     verifyTargetAccess,
     verifyUserHasAction(ActionsEnum.deleteTarget),
-    target.deleteTarget
+    target.deleteTarget,
 );
 
 authenticated.put(
     "/org/:orgId/role",
     verifyOrgAccess,
     verifyUserHasAction(ActionsEnum.createRole),
-    role.createRole
+    role.createRole,
 );
 authenticated.get(
     "/org/:orgId/roles",
     verifyOrgAccess,
     verifyUserHasAction(ActionsEnum.listRoles),
-    role.listRoles
+    role.listRoles,
 );
 // authenticated.get(
 //     "/role/:roleId",
@@ -227,14 +227,14 @@ authenticated.delete(
     "/role/:roleId",
     verifyRoleAccess,
     verifyUserHasAction(ActionsEnum.deleteRole),
-    role.deleteRole
+    role.deleteRole,
 );
 authenticated.post(
     "/role/:roleId/add/:userId",
     verifyRoleAccess,
     verifyUserAccess,
     verifyUserHasAction(ActionsEnum.addUserRole),
-    user.addUserRole
+    user.addUserRole,
 );
 
 // authenticated.put(
@@ -264,7 +264,7 @@ authenticated.post(
     verifyResourceAccess,
     verifyRoleAccess,
     verifyUserHasAction(ActionsEnum.setResourceRoles),
-    resource.setResourceRoles
+    resource.setResourceRoles,
 );
 
 authenticated.post(
@@ -272,19 +272,29 @@ authenticated.post(
     verifyResourceAccess,
     verifySetResourceUsers,
     verifyUserHasAction(ActionsEnum.setResourceUsers),
-    resource.setResourceUsers
+    resource.setResourceUsers,
 );
 
 authenticated.post(
     `/resource/:resourceId/password`,
     verifyResourceAccess,
     verifyUserHasAction(ActionsEnum.setResourceAuthMethods),
-    resource.setResourcePassword
+    resource.setResourcePassword,
 );
-
 unauthenticated.post(
     "/resource/:resourceId/auth/password",
-    resource.authWithPassword
+    resource.authWithPassword,
+);
+
+authenticated.post(
+    `/resource/:resourceId/pincode`,
+    verifyResourceAccess,
+    verifyUserHasAction(ActionsEnum.setResourceAuthMethods),
+    resource.setResourcePincode,
+);
+unauthenticated.post(
+    "/resource/:resourceId/auth/pincode",
+    resource.authWithPincode,
 );
 
 unauthenticated.get("/resource/:resourceId/auth", resource.getResourceAuthInfo);
@@ -325,14 +335,14 @@ authenticated.get(
     "/org/:orgId/users",
     verifyOrgAccess,
     verifyUserHasAction(ActionsEnum.listUsers),
-    user.listUsers
+    user.listUsers,
 );
 authenticated.delete(
     "/org/:orgId/user/:userId",
     verifyOrgAccess,
     verifyUserAccess,
     verifyUserHasAction(ActionsEnum.removeUser),
-    user.removeUserOrg
+    user.removeUserOrg,
 );
 
 // authenticated.put(
@@ -374,7 +384,7 @@ authRouter.use(
         windowMin: 10,
         max: 15,
         type: "IP_AND_PATH",
-    })
+    }),
 );
 
 authRouter.put("/signup", auth.signup);
@@ -386,19 +396,19 @@ authRouter.post("/2fa/enable", verifySessionUserMiddleware, auth.verifyTotp);
 authRouter.post(
     "/2fa/request",
     verifySessionUserMiddleware,
-    auth.requestTotpSecret
+    auth.requestTotpSecret,
 );
 authRouter.post("/2fa/disable", verifySessionUserMiddleware, auth.disable2fa);
 authRouter.post("/verify-email", verifySessionMiddleware, auth.verifyEmail);
 authRouter.post(
     "/verify-email/request",
     verifySessionMiddleware,
-    auth.requestEmailVerificationCode
+    auth.requestEmailVerificationCode,
 );
 authRouter.post(
     "/change-password",
     verifySessionUserMiddleware,
-    auth.changePassword
+    auth.changePassword,
 );
 authRouter.post("/reset-password/request", auth.requestPasswordReset);
 authRouter.post("/reset-password/", auth.resetPassword);

+ 154 - 0
server/routers/resource/authWithPincode.ts

@@ -0,0 +1,154 @@
+import { verify } from "@node-rs/argon2";
+import { generateSessionToken } from "@server/auth";
+import db from "@server/db";
+import { resourcePincode, resources } from "@server/db/schema";
+import HttpCode from "@server/types/HttpCode";
+import response from "@server/utils/response";
+import { eq } from "drizzle-orm";
+import { NextFunction, Request, Response } from "express";
+import createHttpError from "http-errors";
+import { z } from "zod";
+import { fromError } from "zod-validation-error";
+import {
+    createResourceSession,
+    serializeResourceSessionCookie,
+} from "@server/auth/resource";
+import logger from "@server/logger";
+
+export const authWithPincodeBodySchema = z.object({
+    pincode: z.string(),
+    email: z.string().email().optional(),
+    code: z.string().optional(),
+});
+
+export const authWithPincodeParamsSchema = z.object({
+    resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
+});
+
+export type AuthWithPincodeResponse = {
+    codeRequested?: boolean;
+};
+
+export async function authWithPincode(
+    req: Request,
+    res: Response,
+    next: NextFunction,
+): Promise<any> {
+    const parsedBody = authWithPincodeBodySchema.safeParse(req.body);
+
+    if (!parsedBody.success) {
+        return next(
+            createHttpError(
+                HttpCode.BAD_REQUEST,
+                fromError(parsedBody.error).toString(),
+            ),
+        );
+    }
+
+    const parsedParams = authWithPincodeParamsSchema.safeParse(req.params);
+
+    if (!parsedParams.success) {
+        return next(
+            createHttpError(
+                HttpCode.BAD_REQUEST,
+                fromError(parsedParams.error).toString(),
+            ),
+        );
+    }
+
+    const { resourceId } = parsedParams.data;
+    const { email, pincode, code } = parsedBody.data;
+
+    try {
+        const [result] = await db
+            .select()
+            .from(resources)
+            .leftJoin(
+                resourcePincode,
+                eq(resourcePincode.resourceId, resources.resourceId),
+            )
+            .where(eq(resources.resourceId, resourceId))
+            .limit(1);
+
+        const resource = result?.resources;
+        const definedPincode = result?.resourcePincode;
+
+        if (!resource) {
+            return next(
+                createHttpError(
+                    HttpCode.BAD_REQUEST,
+                    "Resource does not exist",
+                ),
+            );
+        }
+
+        if (!definedPincode) {
+            return next(
+                createHttpError(
+                    HttpCode.UNAUTHORIZED,
+                    createHttpError(
+                        HttpCode.BAD_REQUEST,
+                        "Resource has no pincode protection",
+                    ),
+                ),
+            );
+        }
+
+        const validPincode = await verify(definedPincode.pincodeHash, pincode, {
+            memoryCost: 19456,
+            timeCost: 2,
+            outputLen: 32,
+            parallelism: 1,
+        });
+        if (!validPincode) {
+            return next(
+                createHttpError(HttpCode.UNAUTHORIZED, "Incorrect PIN code"),
+            );
+        }
+
+        if (resource.twoFactorEnabled) {
+            if (!code) {
+                return response<AuthWithPincodeResponse>(res, {
+                    data: { codeRequested: true },
+                    success: true,
+                    error: false,
+                    message: "Two-factor authentication required",
+                    status: HttpCode.ACCEPTED,
+                });
+            }
+
+            // TODO: Implement email OTP for resource 2fa
+        }
+
+        const token = generateSessionToken();
+        await createResourceSession({
+            resourceId,
+            token,
+            pincodeId: definedPincode.pincodeId,
+        });
+        const secureCookie = resource.ssl;
+        const cookie = serializeResourceSessionCookie(
+            token,
+            resource.fullDomain,
+            secureCookie,
+        );
+        res.appendHeader("Set-Cookie", cookie);
+
+        logger.debug(cookie); // remove after testing
+
+        return response<null>(res, {
+            data: null,
+            success: true,
+            error: false,
+            message: "Authenticated with resource successfully",
+            status: HttpCode.OK,
+        });
+    } catch (e) {
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "Failed to authenticate with resource",
+            ),
+        );
+    }
+}

+ 2 - 0
server/routers/resource/index.ts

@@ -10,3 +10,5 @@ export * from "./listResourceUsers";
 export * from "./setResourcePassword";
 export * from "./authWithPassword";
 export * from "./getResourceAuthInfo";
+export * from "./setResourcePincode";
+export * from "./authWithPincode";

+ 91 - 0
server/routers/resource/setResourcePincode.ts

@@ -0,0 +1,91 @@
+import { Request, Response, NextFunction } from "express";
+import { z } from "zod";
+import { db } from "@server/db";
+import { resourcePincode } from "@server/db/schema";
+import { eq } from "drizzle-orm";
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { fromError } from "zod-validation-error";
+import { hash } from "@node-rs/argon2";
+import { response } from "@server/utils";
+import stoi from "@server/utils/stoi";
+
+const setResourceAuthMethodsParamsSchema = z.object({
+    resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
+});
+
+const setResourceAuthMethodsBodySchema = z
+    .object({
+        pincode: z
+            .string()
+            .regex(/^\d{6}$/)
+            .or(z.null()),
+    })
+    .strict();
+
+export async function setResourcePincode(
+    req: Request,
+    res: Response,
+    next: NextFunction,
+): Promise<any> {
+    try {
+        const parsedParams = setResourceAuthMethodsParamsSchema.safeParse(
+            req.params,
+        );
+        if (!parsedParams.success) {
+            return next(
+                createHttpError(
+                    HttpCode.BAD_REQUEST,
+                    fromError(parsedParams.error).toString(),
+                ),
+            );
+        }
+
+        const parsedBody = setResourceAuthMethodsBodySchema.safeParse(req.body);
+        if (!parsedBody.success) {
+            return next(
+                createHttpError(
+                    HttpCode.BAD_REQUEST,
+                    fromError(parsedBody.error).toString(),
+                ),
+            );
+        }
+
+        const { resourceId } = parsedParams.data;
+        const { pincode } = parsedBody.data;
+
+        await db.transaction(async (trx) => {
+            await trx
+                .delete(resourcePincode)
+                .where(eq(resourcePincode.resourceId, resourceId));
+
+            if (pincode) {
+                const pincodeHash = await hash(pincode, {
+                    memoryCost: 19456,
+                    timeCost: 2,
+                    outputLen: 32,
+                    parallelism: 1,
+                });
+
+                await trx
+                    .insert(resourcePincode)
+                    .values({ resourceId, pincodeHash, digitLength: 6 });
+            }
+        });
+
+        return response(res, {
+            data: {},
+            success: true,
+            error: false,
+            message: "Resource PIN code set successfully",
+            status: HttpCode.CREATED,
+        });
+    } catch (error) {
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred",
+            ),
+        );
+    }
+}

+ 199 - 0
src/app/[orgId]/settings/resources/[resourceId]/authentication/components/SetResourcePincodeForm.tsx

@@ -0,0 +1,199 @@
+"use client";
+
+import api from "@app/api";
+import { Button } from "@app/components/ui/button";
+import {
+    Form,
+    FormControl,
+    FormDescription,
+    FormField,
+    FormItem,
+    FormLabel,
+    FormMessage,
+} from "@app/components/ui/form";
+import { Input } from "@app/components/ui/input";
+import { useToast } from "@app/hooks/useToast";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useEffect, useState } from "react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+import {
+    Credenza,
+    CredenzaBody,
+    CredenzaClose,
+    CredenzaContent,
+    CredenzaDescription,
+    CredenzaFooter,
+    CredenzaHeader,
+    CredenzaTitle,
+} from "@app/components/Credenza";
+import { formatAxiosError } from "@app/lib/utils";
+import { AxiosResponse } from "axios";
+import { Resource } from "@server/db/schema";
+import {
+    InputOTP,
+    InputOTPGroup,
+    InputOTPSlot,
+} from "@app/components/ui/input-otp";
+
+const setPincodeFormSchema = z.object({
+    pincode: z.string().length(6),
+});
+
+type SetPincodeFormValues = z.infer<typeof setPincodeFormSchema>;
+
+const defaultValues: Partial<SetPincodeFormValues> = {
+    pincode: "",
+};
+
+type SetPincodeFormProps = {
+    open: boolean;
+    setOpen: (open: boolean) => void;
+    resourceId: number;
+    onSetPincode?: () => void;
+};
+
+export default function SetResourcePincodeForm({
+    open,
+    setOpen,
+    resourceId,
+    onSetPincode,
+}: SetPincodeFormProps) {
+    const { toast } = useToast();
+
+    const [loading, setLoading] = useState(false);
+
+    const form = useForm<SetPincodeFormValues>({
+        resolver: zodResolver(setPincodeFormSchema),
+        defaultValues,
+    });
+
+    useEffect(() => {
+        if (!open) {
+            return;
+        }
+
+        form.reset();
+    }, [open]);
+
+    async function onSubmit(data: SetPincodeFormValues) {
+        setLoading(true);
+
+        api.post<AxiosResponse<Resource>>(`/resource/${resourceId}/pincode`, {
+            pincode: data.pincode,
+        })
+            .catch((e) => {
+                toast({
+                    variant: "destructive",
+                    title: "Error setting resource PIN code",
+                    description: formatAxiosError(
+                        e,
+                        "An error occurred while setting the resource PIN code",
+                    ),
+                });
+            })
+            .then(() => {
+                toast({
+                    title: "Resource PIN code set",
+                    description:
+                        "The resource pincode has been set successfully",
+                });
+
+                if (onSetPincode) {
+                    onSetPincode();
+                }
+            })
+            .finally(() => setLoading(false));
+    }
+
+    return (
+        <>
+            <Credenza
+                open={open}
+                onOpenChange={(val) => {
+                    setOpen(val);
+                    setLoading(false);
+                    form.reset();
+                }}
+            >
+                <CredenzaContent>
+                    <CredenzaHeader>
+                        <CredenzaTitle>Set Pincode</CredenzaTitle>
+                        <CredenzaDescription>
+                            Set a pincode to protect this resource
+                        </CredenzaDescription>
+                    </CredenzaHeader>
+                    <CredenzaBody>
+                        <Form {...form}>
+                            <form
+                                onSubmit={form.handleSubmit(onSubmit)}
+                                className="space-y-4"
+                                id="set-pincode-form"
+                            >
+                                <FormField
+                                    control={form.control}
+                                    name="pincode"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>PIN Code</FormLabel>
+                                            <FormControl>
+                                                <div className="flex justify-center">
+                                                    <InputOTP
+                                                        autoComplete="false"
+                                                        maxLength={6}
+                                                        {...field}
+                                                    >
+                                                        <InputOTPGroup className="flex">
+                                                            <InputOTPSlot
+                                                                index={0}
+                                                            />
+                                                            <InputOTPSlot
+                                                                index={1}
+                                                            />
+                                                            <InputOTPSlot
+                                                                index={2}
+                                                            />
+                                                            <InputOTPSlot
+                                                                index={3}
+                                                            />
+                                                            <InputOTPSlot
+                                                                index={4}
+                                                            />
+                                                            <InputOTPSlot
+                                                                index={5}
+                                                            />
+                                                        </InputOTPGroup>
+                                                    </InputOTP>
+                                                </div>
+                                            </FormControl>
+                                            <FormDescription>
+                                                Users will be able to access
+                                                this resource by entering this
+                                                PIN code. It must be at least 6
+                                                digits long.
+                                            </FormDescription>
+                                            <FormMessage />
+                                        </FormItem>
+                                    )}
+                                />
+                            </form>
+                        </Form>
+                    </CredenzaBody>
+                    <CredenzaFooter>
+                        <Button
+                            type="submit"
+                            form="set-pincode-form"
+                            loading={loading}
+                            disabled={loading}
+                        >
+                            Enable PIN Code Protection
+                        </Button>
+                        <CredenzaClose asChild>
+                            <Button variant="outline">Close</Button>
+                        </CredenzaClose>
+                    </CredenzaFooter>
+                </CredenzaContent>
+            </Credenza>
+        </>
+    );
+}

+ 111 - 25
src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx

@@ -32,9 +32,10 @@ import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
 import { ListUsersResponse } from "@server/routers/user";
 import { Switch } from "@app/components/ui/switch";
 import { Label } from "@app/components/ui/label";
-import { ShieldCheck } from "lucide-react";
+import { Binary, Key, ShieldCheck } from "lucide-react";
 import SetResourcePasswordForm from "./components/SetResourcePasswordForm";
 import { Separator } from "@app/components/ui/separator";
+import SetResourcePincodeForm from "./components/SetResourcePincodeForm";
 
 const UsersRolesFormSchema = z.object({
     roles: z.array(
@@ -78,8 +79,11 @@ export default function ResourceAuthenticationPage() {
     const [loadingSaveUsersRoles, setLoadingSaveUsersRoles] = useState(false);
     const [loadingRemoveResourcePassword, setLoadingRemoveResourcePassword] =
         useState(false);
+    const [loadingRemoveResourcePincode, setLoadingRemoveResourcePincode] =
+        useState(false);
 
     const [isSetPasswordOpen, setIsSetPasswordOpen] = useState(false);
+    const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false);
 
     const usersRolesForm = useForm<z.infer<typeof UsersRolesFormSchema>>({
         resolver: zodResolver(UsersRolesFormSchema),
@@ -237,6 +241,36 @@ export default function ResourceAuthenticationPage() {
             .finally(() => setLoadingRemoveResourcePassword(false));
     }
 
+    function removeResourcePincode() {
+        setLoadingRemoveResourcePincode(true);
+
+        api.post(`/resource/${resource.resourceId}/pincode`, {
+            pincode: null,
+        })
+            .then(() => {
+                toast({
+                    title: "Resource pincode removed",
+                    description:
+                        "The resource password has been removed successfully",
+                });
+
+                updateAuthInfo({
+                    pincode: false,
+                });
+            })
+            .catch((e) => {
+                toast({
+                    variant: "destructive",
+                    title: "Error removing resource pincode",
+                    description: formatAxiosError(
+                        e,
+                        "An error occurred while removing the resource pincode",
+                    ),
+                });
+            })
+            .finally(() => setLoadingRemoveResourcePincode(false));
+    }
+
     if (pageLoading) {
         return <></>;
     }
@@ -257,6 +291,20 @@ export default function ResourceAuthenticationPage() {
                 />
             )}
 
+            {isSetPincodeOpen && (
+                <SetResourcePincodeForm
+                    open={isSetPincodeOpen}
+                    setOpen={setIsSetPincodeOpen}
+                    resourceId={resource.resourceId}
+                    onSetPincode={() => {
+                        setIsSetPincodeOpen(false);
+                        updateAuthInfo({
+                            pincode: true,
+                        });
+                    }}
+                />
+            )}
+
             <div className="space-y-12">
                 <section className="space-y-8">
                     <SettingsSectionTitle
@@ -412,40 +460,78 @@ export default function ResourceAuthenticationPage() {
 
                 <Separator />
 
-                <section className="space-y-8">
+                <section className="space-y-8 lg:max-w-2xl">
                     <SettingsSectionTitle
                         title="Authentication Methods"
                         description="Allow anyone to access the resource via the below methods"
                         size="1xl"
                     />
 
-                    {authInfo?.password ? (
-                        <div className="flex items-center space-x-4">
-                            <div className="flex items-center text-green-500 space-x-2">
-                                <ShieldCheck />
-                                <span>Password Protection Enabled</span>
-                            </div>
-                            <Button
-                                variant="gray"
-                                type="button"
-                                loading={loadingRemoveResourcePassword}
-                                disabled={loadingRemoveResourcePassword}
-                                onClick={removeResourcePassword}
+                    <div className="flex flex-col space-y-4">
+                        <div className="flex items-center justify-between space-x-4">
+                            <div
+                                className={`flex items-center text-${!authInfo.password ? "red" : "green"}-500 space-x-2`}
                             >
-                                Remove Password
-                            </Button>
+                                <Key />
+                                <span>
+                                    Password Protection{" "}
+                                    {authInfo?.password
+                                        ? "Enabled"
+                                        : "Disabled"}
+                                </span>
+                            </div>
+                            {authInfo?.password ? (
+                                <Button
+                                    variant="gray"
+                                    type="button"
+                                    loading={loadingRemoveResourcePassword}
+                                    disabled={loadingRemoveResourcePassword}
+                                    onClick={removeResourcePassword}
+                                >
+                                    Remove Password
+                                </Button>
+                            ) : (
+                                <Button
+                                    variant="gray"
+                                    type="button"
+                                    onClick={() => setIsSetPasswordOpen(true)}
+                                >
+                                    Add Password
+                                </Button>
+                            )}
                         </div>
-                    ) : (
-                        <div>
-                            <Button
-                                variant="gray"
-                                type="button"
-                                onClick={() => setIsSetPasswordOpen(true)}
+
+                        <div className="flex items-center justify-between space-x-4">
+                            <div
+                                className={`flex items-center text-${!authInfo.pincode ? "red" : "green"}-500 space-x-2`}
                             >
-                                Add Password
-                            </Button>
+                                <Binary />
+                                <span>
+                                    PIN Code Protection{" "}
+                                    {authInfo?.pincode ? "Enabled" : "Disabled"}
+                                </span>
+                            </div>
+                            {authInfo?.pincode ? (
+                                <Button
+                                    variant="gray"
+                                    type="button"
+                                    loading={loadingRemoveResourcePincode}
+                                    disabled={loadingRemoveResourcePincode}
+                                    onClick={removeResourcePincode}
+                                >
+                                    Remove PIN Code
+                                </Button>
+                            ) : (
+                                <Button
+                                    variant="gray"
+                                    type="button"
+                                    onClick={() => setIsSetPincodeOpen(true)}
+                                >
+                                    Add PIN Code
+                                </Button>
+                            )}
                         </div>
-                    )}
+                    </div>
                 </section>
             </div>
         </>

+ 3 - 1
src/app/auth/layout.tsx

@@ -12,7 +12,9 @@ type AuthLayoutProps = {
 export default async function AuthLayout({ children }: AuthLayoutProps) {
     return (
         <>
-            <div className="p-3 md:mt-32">{children}</div>
+            <div className="w-full max-w-md mx-auto p-3 md:mt-32">
+                {children}
+            </div>
         </>
     );
 }

+ 1 - 1
src/app/auth/login/DashboardLoginForm.tsx

@@ -20,7 +20,7 @@ export default function DashboardLoginForm({
     const router = useRouter();
 
     return (
-        <Card className="w-full max-w-md mx-auto">
+        <Card className="w-full max-w-md">
             <CardHeader>
                 <CardTitle>Login</CardTitle>
                 <CardDescription>

+ 0 - 0
src/app/[orgId]/auth/resource/[resourceId]/components/ResourceAccessDenied.tsx → src/app/auth/resource/[resourceId]/components/ResourceAccessDenied.tsx


+ 31 - 5
src/app/[orgId]/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx → src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx

@@ -68,7 +68,9 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
     const router = useRouter();
 
     const [passwordError, setPasswordError] = useState<string | null>(null);
+    const [pincodeError, setPincodeError] = useState<string | null>(null);
     const [accessDenied, setAccessDenied] = useState<boolean>(false);
+    const [loadingLogin, setLoadingLogin] = useState(false);
 
     function getDefaultSelectedMethod() {
         if (props.methods.sso) {
@@ -111,11 +113,24 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
     });
 
     const onPinSubmit = (values: z.infer<typeof pinSchema>) => {
-        console.log("PIN authentication", values);
-        // Implement PIN authentication logic here
+        setLoadingLogin(true);
+        api.post(`/resource/${props.resource.id}/auth/pincode`, {
+            pincode: values.pin,
+        })
+            .then((res) => {
+                window.location.href = props.redirect;
+            })
+            .catch((e) => {
+                console.error(e);
+                setPincodeError(
+                    formatAxiosError(e, "Failed to authenticate with pincode"),
+                );
+            })
+            .then(() => setLoadingLogin(false));
     };
 
     const onPasswordSubmit = (values: z.infer<typeof passwordSchema>) => {
+        setLoadingLogin(true);
         api.post(`/resource/${props.resource.id}/auth/password`, {
             password: values.password,
         })
@@ -127,7 +142,8 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
                 setPasswordError(
                     formatAxiosError(e, "Failed to authenticate with password"),
                 );
-            });
+            })
+            .finally(() => setLoadingLogin(false));
     };
 
     async function handleSSOAuth() {
@@ -202,8 +218,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
                                                     render={({ field }) => (
                                                         <FormItem>
                                                             <FormLabel>
-                                                                Enter 6-digit
-                                                                PIN
+                                                                6-digit PIN Code
                                                             </FormLabel>
                                                             <FormControl>
                                                                 <div className="flex justify-center">
@@ -252,9 +267,18 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
                                                         </FormItem>
                                                     )}
                                                 />
+                                                {pincodeError && (
+                                                    <Alert variant="destructive">
+                                                        <AlertDescription>
+                                                            {pincodeError}
+                                                        </AlertDescription>
+                                                    </Alert>
+                                                )}
                                                 <Button
                                                     type="submit"
                                                     className="w-full"
+                                                    loading={loadingLogin}
+                                                    disabled={loadingLogin}
                                                 >
                                                     <LockIcon className="w-4 h-4 mr-2" />
                                                     Login with PIN
@@ -306,6 +330,8 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
                                                 <Button
                                                     type="submit"
                                                     className="w-full"
+                                                    loading={loadingLogin}
+                                                    disabled={loadingLogin}
                                                 >
                                                     <LockIcon className="w-4 h-4 mr-2" />
                                                     Login with Password

+ 0 - 0
src/app/[orgId]/auth/resource/[resourceId]/components/ResourceNotFound.tsx → src/app/auth/resource/[resourceId]/components/ResourceNotFound.tsx


+ 10 - 15
src/app/[orgId]/auth/resource/[resourceId]/page.tsx → src/app/auth/resource/[resourceId]/page.tsx

@@ -13,7 +13,7 @@ import ResourceNotFound from "./components/ResourceNotFound";
 import ResourceAccessDenied from "./components/ResourceAccessDenied";
 
 export default async function ResourceAuthPage(props: {
-    params: Promise<{ resourceId: number; orgId: string }>;
+    params: Promise<{ resourceId: number }>;
     searchParams: Promise<{ r: string }>;
 }) {
     const params = await props.params;
@@ -28,17 +28,14 @@ export default async function ResourceAuthPage(props: {
         if (res && res.status === 200) {
             authInfo = res.data.data;
         }
-    } catch (e) {
-        console.error(e);
-        console.log("resource not found");
-    }
+    } catch (e) {}
 
     const getUser = cache(verifySession);
     const user = await getUser();
 
     if (!authInfo) {
         return (
-            <div className="w-full max-w-md mx-auto p-3 md:mt-32">
+            <div className="w-full max-w-md">
                 <ResourceNotFound />
             </div>
         );
@@ -47,12 +44,10 @@ export default async function ResourceAuthPage(props: {
     const hasAuth = authInfo.password || authInfo.pincode || authInfo.sso;
     const isSSOOnly = authInfo.sso && !authInfo.password && !authInfo.pincode;
 
+    const redirectUrl = searchParams.r || authInfo.url;
+
     if (!hasAuth) {
-        return (
-            <div className="w-full max-w-md mx-auto p-3 md:mt-32">
-                <ResourceAccessDenied />
-            </div>
-        );
+        redirect(redirectUrl);
     }
 
     let userIsUnauthorized = false;
@@ -72,13 +67,13 @@ export default async function ResourceAuthPage(props: {
         }
 
         if (doRedirect) {
-            redirect(searchParams.r || authInfo.url);
+            redirect(redirectUrl);
         }
     }
 
     if (userIsUnauthorized && isSSOOnly) {
         return (
-            <div className="w-full max-w-md mx-auto p-3 md:mt-32">
+            <div className="w-full max-w-md">
                 <ResourceAccessDenied />
             </div>
         );
@@ -86,7 +81,7 @@ export default async function ResourceAuthPage(props: {
 
     return (
         <>
-            <div className="w-full max-w-md mx-auto p-3 md:mt-32">
+            <div className="w-full max-w-md">
                 <ResourceAuthPortal
                     methods={{
                         password: authInfo.password,
@@ -97,7 +92,7 @@ export default async function ResourceAuthPage(props: {
                         name: authInfo.resourceName,
                         id: authInfo.resourceId,
                     }}
-                    redirect={searchParams.r || authInfo.url}
+                    redirect={redirectUrl}
                 />
             </div>
         </>

+ 1 - 1
src/app/auth/signup/SignupForm.tsx

@@ -100,7 +100,7 @@ export default function SignupForm({ redirect }: SignupFormProps) {
     }
 
     return (
-        <Card className="w-full max-w-md mx-auto">
+        <Card className="w-full max-w-md">
             <CardHeader>
                 <CardTitle>Create Account</CardTitle>
                 <CardDescription>

+ 1 - 1
src/app/auth/verify-email/VerifyEmailForm.tsx

@@ -123,7 +123,7 @@ export default function VerifyEmailForm({
 
     return (
         <div>
-            <Card className="w-full max-w-md mx-auto">
+            <Card className="w-full max-w-md">
                 <CardHeader>
                     <CardTitle>Verify Your Email</CardTitle>
                     <CardDescription>