diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index 80805d9c..4ab4d085 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -90,7 +90,7 @@ export async function verifyResourceSession( return allowed(res); } - const redirectUrl = `${config.app.base_url}/${resource.orgId}/auth/resource/${resource.resourceId}?redirect=${originalRequestURL}`; + const redirectUrl = `${config.app.base_url}/${resource.orgId}/auth/resource/${resource.resourceId}?r=${originalRequestURL}`; if (sso && sessions.session) { const { session, user } = await validateSessionToken( diff --git a/server/routers/resource/getResourceAuthInfo.ts b/server/routers/resource/getResourceAuthInfo.ts index df08d4bd..c2afe95a 100644 --- a/server/routers/resource/getResourceAuthInfo.ts +++ b/server/routers/resource/getResourceAuthInfo.ts @@ -23,12 +23,13 @@ export type GetResourceAuthInfoResponse = { pincode: boolean; sso: boolean; blockAccess: boolean; + url: string; }; export async function getResourceAuthInfo( req: Request, res: Response, - next: NextFunction + next: NextFunction, ): Promise { try { const parsedParams = getResourceAuthInfoSchema.safeParse(req.params); @@ -36,8 +37,8 @@ export async function getResourceAuthInfo( return next( createHttpError( HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) + fromError(parsedParams.error).toString(), + ), ); } @@ -48,11 +49,11 @@ export async function getResourceAuthInfo( .from(resources) .leftJoin( resourcePincode, - eq(resourcePincode.resourceId, resources.resourceId) + eq(resourcePincode.resourceId, resources.resourceId), ) .leftJoin( resourcePassword, - eq(resourcePassword.resourceId, resources.resourceId) + eq(resourcePassword.resourceId, resources.resourceId), ) .where(eq(resources.resourceId, resourceId)) .limit(1); @@ -61,9 +62,11 @@ export async function getResourceAuthInfo( const pincode = result?.resourcePincode; const password = result?.resourcePassword; + const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`; + if (!resource) { return next( - createHttpError(HttpCode.NOT_FOUND, "Resource not found") + createHttpError(HttpCode.NOT_FOUND, "Resource not found"), ); } @@ -75,6 +78,7 @@ export async function getResourceAuthInfo( pincode: pincode !== null, sso: resource.sso, blockAccess: resource.blockAccess, + url, }, success: true, error: false, @@ -83,7 +87,10 @@ export async function getResourceAuthInfo( }); } catch (error) { return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "An error occurred", + ), ); } } diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 06e98fbb..34107fc8 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -31,7 +31,7 @@ const updateResourceBodySchema = z export async function updateResource( req: Request, res: Response, - next: NextFunction + next: NextFunction, ): Promise { try { const parsedParams = updateResourceParamsSchema.safeParse(req.params); @@ -39,8 +39,8 @@ export async function updateResource( return next( createHttpError( HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) + fromError(parsedParams.error).toString(), + ), ); } @@ -49,8 +49,8 @@ export async function updateResource( return next( createHttpError( HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) + fromError(parsedBody.error).toString(), + ), ); } @@ -67,8 +67,8 @@ export async function updateResource( return next( createHttpError( HttpCode.NOT_FOUND, - `Resource with ID ${resourceId} not found` - ) + `Resource with ID ${resourceId} not found`, + ), ); } @@ -76,16 +76,23 @@ export async function updateResource( return next( createHttpError( HttpCode.BAD_REQUEST, - "Resource does not have a domain" - ) + "Resource does not have a domain", + ), ); } - const fullDomain = `${updateData.subdomain}.${resource[0].orgs.domain}`; + const fullDomain = updateData.subdomain + ? `${updateData.subdomain}.${resource[0].orgs.domain}` + : undefined; + + const updatePayload = { + ...updateData, + ...(fullDomain && { fullDomain }), + }; const updatedResource = await db .update(resources) - .set({ ...updateData, fullDomain }) + .set(updatePayload) .where(eq(resources.resourceId, resourceId)) .returning(); @@ -93,8 +100,8 @@ export async function updateResource( return next( createHttpError( HttpCode.NOT_FOUND, - `Resource with ID ${resourceId} not found` - ) + `Resource with ID ${resourceId} not found`, + ), ); } @@ -108,7 +115,10 @@ export async function updateResource( } catch (error) { logger.error(error); return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "An error occurred", + ), ); } } diff --git a/src/app/[orgId]/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx b/src/app/[orgId]/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx index 761f0fb1..7879f3f2 100644 --- a/src/app/[orgId]/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx +++ b/src/app/[orgId]/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx @@ -29,6 +29,12 @@ import { InputOTPGroup, InputOTPSlot, } from "@app/components/ui/input-otp"; +import api from "@app/api"; +import { useRouter } from "next/navigation"; +import { Alert, AlertDescription } from "@app/components/ui/alert"; +import { formatAxiosError } from "@app/lib/utils"; +import { AxiosResponse } from "axios"; +import { LoginResponse } from "@server/routers/auth"; const pinSchema = z.object({ pin: z @@ -40,16 +46,60 @@ const pinSchema = z.object({ const passwordSchema = z.object({ password: z .string() - .min(8, { message: "Password must be at least 8 characters long" }), + .min(1, { message: "Password must be at least 1 character long" }), }); const userSchema = z.object({ - email: z.string().email(), - password: z.string().min(1), + email: z.string().email({ message: "Please enter a valid email address" }), + password: z + .string() + .min(1, { message: "Password must be at least 1 character long" }), }); -export default function ResourceAuthPortal() { - const [activeTab, setActiveTab] = useState("pin"); +type ResourceAuthPortalProps = { + methods: { + password: boolean; + pincode: boolean; + sso: boolean; + }; + resource: { + name: string; + id: number; + }; + redirect: string; +}; + +export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { + const router = useRouter(); + + const [passwordError, setPasswordError] = useState(null); + const [userError, setUserError] = useState(null); + + function getDefaultSelectedMethod() { + if (props.methods.sso) { + return "sso"; + } + + if (props.methods.password) { + return "password"; + } + + if (props.methods.pincode) { + return "pin"; + } + } + + const [activeTab, setActiveTab] = useState(getDefaultSelectedMethod()); + + const getColLength = () => { + let colLength = 0; + if (props.methods.pincode) colLength++; + if (props.methods.password) colLength++; + if (props.methods.sso) colLength++; + return colLength; + }; + + const [numMethods, setNumMethods] = useState(getColLength()); const pinForm = useForm>({ resolver: zodResolver(pinSchema), @@ -79,178 +129,256 @@ export default function ResourceAuthPortal() { }; const onPasswordSubmit = (values: z.infer) => { - console.log("Password authentication", values); - // Implement password authentication logic here + api.post(`/resource/${props.resource.id}/auth/password`, { + password: values.password, + }) + .then((res) => { + window.location.href = props.redirect; + }) + .catch((e) => { + console.error(e); + setPasswordError( + formatAxiosError(e, "Failed to authenticate with password"), + ); + }); }; - const handleSSOAuth = () => { + const handleSSOAuth = (values: z.infer) => { console.log("SSO authentication"); - // Implement SSO authentication logic here + + api.post>("/auth/login", { + email: values.email, + password: values.password, + }) + .then((res) => { + // console.log(res) + window.location.href = props.redirect; + }) + .catch((e) => { + console.error(e); + setUserError( + formatAxiosError(e, "An error occurred while logging in"), + ); + }); }; return ( -
+
Authentication Required - Choose your preferred method + {numMethods > 1 + ? `Choose your preferred method to access ${props.resource.name}` + : `You must authenticate to access ${props.resource.name}`} - - - PIN - - - Password - - - User - - - -
- - ( - - - Enter 6-digit PIN - - -
- 1 && ( + + {props.methods.pincode && ( + + PIN + + )} + {props.methods.password && ( + + {" "} + Password + + )} + {props.methods.sso && ( + + User + + )} + + )} + {props.methods.pincode && ( + + + + ( + + + Enter 6-digit PIN + + +
+ + + + + + + + + + +
+
+ +
+ )} + /> + + + +
+ )} + {props.methods.password && ( + +
+ + ( + + + Password + + + - - - - - - - - - -
-
- -
+ /> + + + + )} + /> + {passwordError && ( + + + {passwordError} + + )} - /> - - - -
- -
- - ( - - Password - - - - - + + + +
+ )} + {props.methods.sso && ( + +
+ - -
- -
- -
- { - console.log( - "User authentication", - values, - ); - // Implement user authentication logic here - }, - )} - className="space-y-4" - > - ( - - Email - - - - - + className="space-y-4" + > + ( + + Email + + + + + + )} + /> + ( + + + Password + + + + + + + )} + /> + {userError && ( + + + {userError} + + )} - /> - ( - - Password - - - - - - )} - /> - - - -
+ + + + + )}
diff --git a/src/app/[orgId]/auth/resource/[resourceId]/components/ResourceNotFound.tsx b/src/app/[orgId]/auth/resource/[resourceId]/components/ResourceNotFound.tsx new file mode 100644 index 00000000..cd3a5068 --- /dev/null +++ b/src/app/[orgId]/auth/resource/[resourceId]/components/ResourceNotFound.tsx @@ -0,0 +1,24 @@ +// import { Card, CardContent, CardHeader, CardTitle } from "@app/components/ui/card"; + +// export default async function ResourceNotFound() { +// return ( +// +// +// {/*
+//
*/} +// +// Invite Not Accepted +// +//
+// {renderBody()} + +// +// {renderFooter()} +// +//
+// ); +// } diff --git a/src/app/[orgId]/auth/resource/[resourceId]/page.tsx b/src/app/[orgId]/auth/resource/[resourceId]/page.tsx index 11e4a02b..4a3b80dd 100644 --- a/src/app/[orgId]/auth/resource/[resourceId]/page.tsx +++ b/src/app/[orgId]/auth/resource/[resourceId]/page.tsx @@ -1,16 +1,85 @@ +import { + GetResourceAuthInfoResponse, + GetResourceResponse, +} from "@server/routers/resource"; import ResourceAuthPortal from "./components/ResourceAuthPortal"; +import { internal } from "@app/api"; +import { AxiosResponse } from "axios"; +import { authCookieHeader } from "@app/api/cookies"; +import { cache } from "react"; +import { verifySession } from "@app/lib/auth/verifySession"; +import { redirect } from "next/navigation"; export default async function ResourceAuthPage(props: { params: Promise<{ resourceId: number; orgId: string }>; + searchParams: Promise<{ r: string }>; }) { const params = await props.params; + const searchParams = await props.searchParams; - console.log(params); + let authInfo: GetResourceAuthInfoResponse | undefined; + try { + const res = await internal.get< + AxiosResponse + >(`/resource/${params.resourceId}/auth`, await authCookieHeader()); + + if (res && res.status === 200) { + authInfo = res.data.data; + } + } catch (e) { + console.error(e); + console.log("resource not found"); + } + + const getUser = cache(verifySession); + const user = await getUser(); + + if (!authInfo) { + return <>Resource not found; + } + + const isSSOOnly = authInfo.sso && !authInfo.password && !authInfo.pincode; + + let userIsUnauthorized = false; + if (user && authInfo.sso) { + let doRedirect = false; + try { + const res = await internal.get>( + `/resource/${params.resourceId}`, + await authCookieHeader(), + ); + + console.log(res.data); + doRedirect = true; + } catch (e) { + console.error(e); + userIsUnauthorized = true; + } + + if (doRedirect) { + redirect(searchParams.r || authInfo.url); + } + } + + if (userIsUnauthorized && isSSOOnly) { + return <>You do not have access to this resource; + } return ( <> -
- +
+
); diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx index 88c2379f..d4d983b4 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx @@ -415,7 +415,7 @@ export default function ResourceAuthenticationPage() {
diff --git a/src/app/[orgId]/settings/resources/[resourceId]/components/ResourceInfoBox.tsx b/src/app/[orgId]/settings/resources/[resourceId]/components/ResourceInfoBox.tsx index f4accb9f..acd33300 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/components/ResourceInfoBox.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/components/ResourceInfoBox.tsx @@ -39,66 +39,64 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { }; return ( - - - - - Resource Information - - -
-
- {authInfo.password || - authInfo.pincode || - authInfo.sso ? ( -
- - - This resource is protected with at least - one auth method. - -
- ) : ( -
- - - This resource is not protected with any - auth method. Anyone can access this - resource. - -
- )} -
- -
- - - {fullUrl} - - -
+
+ ) : ( +
+ + + This resource is not protected with any auth + method. Anyone can access this resource. + +
+ )} +
- {/*

+

+ + + {fullUrl} + + +
+ + {/*

To create a proxy to your private services,{" "} {" "} to this resource

*/} -
- - - +
+ + ); } diff --git a/src/app/invite/InviteStatusCard.tsx b/src/app/invite/InviteStatusCard.tsx index 185cc994..ac9e94a7 100644 --- a/src/app/invite/InviteStatusCard.tsx +++ b/src/app/invite/InviteStatusCard.tsx @@ -99,12 +99,12 @@ export default function InviteStatusCard({
-
+ {/*
+
*/} Invite Not Accepted diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx index 67e238b0..80737796 100644 --- a/src/components/ui/alert.tsx +++ b/src/components/ui/alert.tsx @@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const alertVariants = cva( - "relative w-full rounded-lg p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", { variants: { variant: { diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 5b8e64f7..ccf56da4 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -1,6 +1,6 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Card = React.forwardRef< HTMLDivElement, @@ -9,13 +9,13 @@ const Card = React.forwardRef<
-)) -Card.displayName = "Card" +)); +Card.displayName = "Card"; const CardHeader = React.forwardRef< HTMLDivElement, @@ -26,8 +26,8 @@ const CardHeader = React.forwardRef< className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} /> -)) -CardHeader.displayName = "CardHeader" +)); +CardHeader.displayName = "CardHeader"; const CardTitle = React.forwardRef< HTMLParagraphElement, @@ -37,12 +37,12 @@ const CardTitle = React.forwardRef< ref={ref} className={cn( "text-2xl font-semibold leading-none tracking-tight", - className + className, )} {...props} /> -)) -CardTitle.displayName = "CardTitle" +)); +CardTitle.displayName = "CardTitle"; const CardDescription = React.forwardRef< HTMLParagraphElement, @@ -53,16 +53,16 @@ const CardDescription = React.forwardRef< className={cn("text-sm text-muted-foreground", className)} {...props} /> -)) -CardDescription.displayName = "CardDescription" +)); +CardDescription.displayName = "CardDescription"; const CardContent = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
-)) -CardContent.displayName = "CardContent" +)); +CardContent.displayName = "CardContent"; const CardFooter = React.forwardRef< HTMLDivElement, @@ -73,7 +73,14 @@ const CardFooter = React.forwardRef< className={cn("flex items-center p-6 pt-0", className)} {...props} /> -)) -CardFooter.displayName = "CardFooter" +)); +CardFooter.displayName = "CardFooter"; -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx index 26eb1091..c1f10d85 100644 --- a/src/components/ui/tabs.tsx +++ b/src/components/ui/tabs.tsx @@ -29,7 +29,7 @@ const TabsTrigger = React.forwardRef<