mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-12 15:04:53 +02:00
basic auth portal save
This commit is contained in:
parent
f9e0c33368
commit
0b3ca5f999
12 changed files with 511 additions and 269 deletions
|
@ -90,7 +90,7 @@ export async function verifyResourceSession(
|
||||||
return allowed(res);
|
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) {
|
if (sso && sessions.session) {
|
||||||
const { session, user } = await validateSessionToken(
|
const { session, user } = await validateSessionToken(
|
||||||
|
|
|
@ -23,12 +23,13 @@ export type GetResourceAuthInfoResponse = {
|
||||||
pincode: boolean;
|
pincode: boolean;
|
||||||
sso: boolean;
|
sso: boolean;
|
||||||
blockAccess: boolean;
|
blockAccess: boolean;
|
||||||
|
url: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getResourceAuthInfo(
|
export async function getResourceAuthInfo(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const parsedParams = getResourceAuthInfoSchema.safeParse(req.params);
|
const parsedParams = getResourceAuthInfoSchema.safeParse(req.params);
|
||||||
|
@ -36,8 +37,8 @@ export async function getResourceAuthInfo(
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
fromError(parsedParams.error).toString()
|
fromError(parsedParams.error).toString(),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,11 +49,11 @@ export async function getResourceAuthInfo(
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
resourcePincode,
|
resourcePincode,
|
||||||
eq(resourcePincode.resourceId, resources.resourceId)
|
eq(resourcePincode.resourceId, resources.resourceId),
|
||||||
)
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
resourcePassword,
|
resourcePassword,
|
||||||
eq(resourcePassword.resourceId, resources.resourceId)
|
eq(resourcePassword.resourceId, resources.resourceId),
|
||||||
)
|
)
|
||||||
.where(eq(resources.resourceId, resourceId))
|
.where(eq(resources.resourceId, resourceId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
@ -61,9 +62,11 @@ export async function getResourceAuthInfo(
|
||||||
const pincode = result?.resourcePincode;
|
const pincode = result?.resourcePincode;
|
||||||
const password = result?.resourcePassword;
|
const password = result?.resourcePassword;
|
||||||
|
|
||||||
|
const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
|
||||||
|
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
return next(
|
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,
|
pincode: pincode !== null,
|
||||||
sso: resource.sso,
|
sso: resource.sso,
|
||||||
blockAccess: resource.blockAccess,
|
blockAccess: resource.blockAccess,
|
||||||
|
url,
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
|
@ -83,7 +87,10 @@ export async function getResourceAuthInfo(
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"An error occurred",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ const updateResourceBodySchema = z
|
||||||
export async function updateResource(
|
export async function updateResource(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const parsedParams = updateResourceParamsSchema.safeParse(req.params);
|
const parsedParams = updateResourceParamsSchema.safeParse(req.params);
|
||||||
|
@ -39,8 +39,8 @@ export async function updateResource(
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
fromError(parsedParams.error).toString()
|
fromError(parsedParams.error).toString(),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,8 +49,8 @@ export async function updateResource(
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
fromError(parsedBody.error).toString()
|
fromError(parsedBody.error).toString(),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,8 +67,8 @@ export async function updateResource(
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.NOT_FOUND,
|
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(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
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
|
const updatedResource = await db
|
||||||
.update(resources)
|
.update(resources)
|
||||||
.set({ ...updateData, fullDomain })
|
.set(updatePayload)
|
||||||
.where(eq(resources.resourceId, resourceId))
|
.where(eq(resources.resourceId, resourceId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
@ -93,8 +100,8 @@ export async function updateResource(
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.NOT_FOUND,
|
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) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"An error occurred",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,12 @@ import {
|
||||||
InputOTPGroup,
|
InputOTPGroup,
|
||||||
InputOTPSlot,
|
InputOTPSlot,
|
||||||
} from "@app/components/ui/input-otp";
|
} 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({
|
const pinSchema = z.object({
|
||||||
pin: z
|
pin: z
|
||||||
|
@ -40,16 +46,60 @@ const pinSchema = z.object({
|
||||||
const passwordSchema = z.object({
|
const passwordSchema = z.object({
|
||||||
password: z
|
password: z
|
||||||
.string()
|
.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({
|
const userSchema = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email({ message: "Please enter a valid email address" }),
|
||||||
password: z.string().min(1),
|
password: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Password must be at least 1 character long" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function ResourceAuthPortal() {
|
type ResourceAuthPortalProps = {
|
||||||
const [activeTab, setActiveTab] = useState("pin");
|
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<string | null>(null);
|
||||||
|
const [userError, setUserError] = useState<string | null>(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<z.infer<typeof pinSchema>>({
|
const pinForm = useForm<z.infer<typeof pinSchema>>({
|
||||||
resolver: zodResolver(pinSchema),
|
resolver: zodResolver(pinSchema),
|
||||||
|
@ -79,178 +129,256 @@ export default function ResourceAuthPortal() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPasswordSubmit = (values: z.infer<typeof passwordSchema>) => {
|
const onPasswordSubmit = (values: z.infer<typeof passwordSchema>) => {
|
||||||
console.log("Password authentication", values);
|
api.post(`/resource/${props.resource.id}/auth/password`, {
|
||||||
// Implement password authentication logic here
|
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<typeof userSchema>) => {
|
||||||
console.log("SSO authentication");
|
console.log("SSO authentication");
|
||||||
// Implement SSO authentication logic here
|
|
||||||
|
api.post<AxiosResponse<LoginResponse>>("/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 (
|
return (
|
||||||
<div className="w-full max-w-md mx-auto">
|
<div>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Authentication Required</CardTitle>
|
<CardTitle>Authentication Required</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Choose your preferred method
|
{numMethods > 1
|
||||||
|
? `Choose your preferred method to access ${props.resource.name}`
|
||||||
|
: `You must authenticate to access ${props.resource.name}`}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
<TabsList className="grid w-full grid-cols-3">
|
{numMethods > 1 && (
|
||||||
<TabsTrigger value="pin">
|
<TabsList
|
||||||
<Binary className="w-4 h-4 mr-1" /> PIN
|
className={`grid w-full grid-cols-${numMethods}`}
|
||||||
</TabsTrigger>
|
>
|
||||||
<TabsTrigger value="password">
|
{props.methods.pincode && (
|
||||||
<Key className="w-4 h-4 mr-1" /> Password
|
<TabsTrigger value="pin">
|
||||||
</TabsTrigger>
|
<Binary className="w-4 h-4 mr-1" /> PIN
|
||||||
<TabsTrigger value="sso">
|
</TabsTrigger>
|
||||||
<User className="w-4 h-4 mr-1" /> User
|
)}
|
||||||
</TabsTrigger>
|
{props.methods.password && (
|
||||||
</TabsList>
|
<TabsTrigger value="password">
|
||||||
<TabsContent value="pin">
|
<Key className="w-4 h-4 mr-1" />{" "}
|
||||||
<Form {...pinForm}>
|
Password
|
||||||
<form
|
</TabsTrigger>
|
||||||
onSubmit={pinForm.handleSubmit(onPinSubmit)}
|
)}
|
||||||
className="space-y-4"
|
{props.methods.sso && (
|
||||||
>
|
<TabsTrigger value="sso">
|
||||||
<FormField
|
<User className="w-4 h-4 mr-1" /> User
|
||||||
control={pinForm.control}
|
</TabsTrigger>
|
||||||
name="pin"
|
)}
|
||||||
render={({ field }) => (
|
</TabsList>
|
||||||
<FormItem>
|
)}
|
||||||
<FormLabel>
|
{props.methods.pincode && (
|
||||||
Enter 6-digit PIN
|
<TabsContent value="pin">
|
||||||
</FormLabel>
|
<Form {...pinForm}>
|
||||||
<FormControl>
|
<form
|
||||||
<div className="flex justify-center">
|
onSubmit={pinForm.handleSubmit(
|
||||||
<InputOTP
|
onPinSubmit,
|
||||||
maxLength={6}
|
)}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={pinForm.control}
|
||||||
|
name="pin"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
Enter 6-digit PIN
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<InputOTP
|
||||||
|
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>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<LockIcon className="w-4 h-4 mr-2" />
|
||||||
|
Login with PIN
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
{props.methods.password && (
|
||||||
|
<TabsContent value="password">
|
||||||
|
<Form {...passwordForm}>
|
||||||
|
<form
|
||||||
|
onSubmit={passwordForm.handleSubmit(
|
||||||
|
onPasswordSubmit,
|
||||||
|
)}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={passwordForm.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
Password
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter password"
|
||||||
|
type="password"
|
||||||
{...field}
|
{...field}
|
||||||
>
|
/>
|
||||||
<InputOTPGroup className="flex">
|
</FormControl>
|
||||||
<InputOTPSlot
|
<FormMessage />
|
||||||
index={0}
|
</FormItem>
|
||||||
/>
|
)}
|
||||||
<InputOTPSlot
|
/>
|
||||||
index={1}
|
{passwordError && (
|
||||||
/>
|
<Alert variant="destructive">
|
||||||
<InputOTPSlot
|
<AlertDescription>
|
||||||
index={2}
|
{passwordError}
|
||||||
/>
|
</AlertDescription>
|
||||||
<InputOTPSlot
|
</Alert>
|
||||||
index={3}
|
|
||||||
/>
|
|
||||||
<InputOTPSlot
|
|
||||||
index={4}
|
|
||||||
/>
|
|
||||||
<InputOTPSlot
|
|
||||||
index={5}
|
|
||||||
/>
|
|
||||||
</InputOTPGroup>
|
|
||||||
</InputOTP>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
)}
|
||||||
/>
|
<Button
|
||||||
<Button type="submit" className="w-full">
|
type="submit"
|
||||||
<LockIcon className="w-4 h-4 mr-2" />
|
className="w-full"
|
||||||
Login with PIN
|
>
|
||||||
</Button>
|
<LockIcon className="w-4 h-4 mr-2" />
|
||||||
</form>
|
Login with Password
|
||||||
</Form>
|
</Button>
|
||||||
</TabsContent>
|
</form>
|
||||||
<TabsContent value="password">
|
</Form>
|
||||||
<Form {...passwordForm}>
|
</TabsContent>
|
||||||
<form
|
)}
|
||||||
onSubmit={passwordForm.handleSubmit(
|
{props.methods.sso && (
|
||||||
onPasswordSubmit,
|
<TabsContent value="sso">
|
||||||
)}
|
<Form {...userForm}>
|
||||||
className="space-y-4"
|
<form
|
||||||
>
|
onSubmit={userForm.handleSubmit(
|
||||||
<FormField
|
handleSSOAuth,
|
||||||
control={passwordForm.control}
|
|
||||||
name="password"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Password</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Enter password"
|
|
||||||
type="password"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
)}
|
||||||
/>
|
className="space-y-4"
|
||||||
<Button type="submit" className="w-full">
|
>
|
||||||
<LockIcon className="w-4 h-4 mr-2" />
|
<FormField
|
||||||
Login with Password
|
control={userForm.control}
|
||||||
</Button>
|
name="email"
|
||||||
</form>
|
render={({ field }) => (
|
||||||
</Form>
|
<FormItem>
|
||||||
</TabsContent>
|
<FormLabel>Email</FormLabel>
|
||||||
<TabsContent value="sso">
|
<FormControl>
|
||||||
<Form {...userForm}>
|
<Input
|
||||||
<form
|
placeholder="Enter email"
|
||||||
onSubmit={userForm.handleSubmit(
|
type="email"
|
||||||
(values) => {
|
{...field}
|
||||||
console.log(
|
/>
|
||||||
"User authentication",
|
</FormControl>
|
||||||
values,
|
<FormMessage />
|
||||||
);
|
</FormItem>
|
||||||
// Implement user authentication logic here
|
)}
|
||||||
},
|
/>
|
||||||
)}
|
<FormField
|
||||||
className="space-y-4"
|
control={userForm.control}
|
||||||
>
|
name="password"
|
||||||
<FormField
|
render={({ field }) => (
|
||||||
control={userForm.control}
|
<FormItem>
|
||||||
name="email"
|
<FormLabel>
|
||||||
render={({ field }) => (
|
Password
|
||||||
<FormItem>
|
</FormLabel>
|
||||||
<FormLabel>Email</FormLabel>
|
<FormControl>
|
||||||
<FormControl>
|
<Input
|
||||||
<Input
|
placeholder="Enter password"
|
||||||
placeholder="Enter email"
|
type="password"
|
||||||
type="email"
|
{...field}
|
||||||
{...field}
|
/>
|
||||||
/>
|
</FormControl>
|
||||||
</FormControl>
|
<FormMessage />
|
||||||
<FormMessage />
|
</FormItem>
|
||||||
</FormItem>
|
)}
|
||||||
|
/>
|
||||||
|
{userError && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertDescription>
|
||||||
|
{userError}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
)}
|
)}
|
||||||
/>
|
<Button
|
||||||
<FormField
|
type="submit"
|
||||||
control={userForm.control}
|
className="w-full"
|
||||||
name="password"
|
>
|
||||||
render={({ field }) => (
|
<LockIcon className="w-4 h-4 mr-2" />
|
||||||
<FormItem>
|
Login as User
|
||||||
<FormLabel>Password</FormLabel>
|
</Button>
|
||||||
<FormControl>
|
</form>
|
||||||
<Input
|
</Form>
|
||||||
placeholder="Enter password"
|
</TabsContent>
|
||||||
type="password"
|
)}
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Button type="submit" className="w-full">
|
|
||||||
<LockIcon className="w-4 h-4 mr-2" />
|
|
||||||
Login as User
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
// import { Card, CardContent, CardHeader, CardTitle } from "@app/components/ui/card";
|
||||||
|
|
||||||
|
// export default async function ResourceNotFound() {
|
||||||
|
// return (
|
||||||
|
// <Card className="w-full max-w-md">
|
||||||
|
// <CardHeader>
|
||||||
|
// {/* <div className="flex items-center justify-center w-20 h-20 rounded-full bg-red-100 mx-auto mb-4">
|
||||||
|
// <XCircle
|
||||||
|
// className="w-10 h-10 text-red-600"
|
||||||
|
// aria-hidden="true"
|
||||||
|
// />
|
||||||
|
// </div> */}
|
||||||
|
// <CardTitle className="text-center text-2xl font-bold">
|
||||||
|
// Invite Not Accepted
|
||||||
|
// </CardTitle>
|
||||||
|
// </CardHeader>
|
||||||
|
// <CardContent>{renderBody()}</CardContent>
|
||||||
|
|
||||||
|
// <CardFooter className="flex justify-center space-x-4">
|
||||||
|
// {renderFooter()}
|
||||||
|
// </CardFooter>
|
||||||
|
// </Card>
|
||||||
|
// );
|
||||||
|
// }
|
|
@ -1,16 +1,85 @@
|
||||||
|
import {
|
||||||
|
GetResourceAuthInfoResponse,
|
||||||
|
GetResourceResponse,
|
||||||
|
} from "@server/routers/resource";
|
||||||
import ResourceAuthPortal from "./components/ResourceAuthPortal";
|
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: {
|
export default async function ResourceAuthPage(props: {
|
||||||
params: Promise<{ resourceId: number; orgId: string }>;
|
params: Promise<{ resourceId: number; orgId: string }>;
|
||||||
|
searchParams: Promise<{ r: string }>;
|
||||||
}) {
|
}) {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
|
|
||||||
console.log(params);
|
let authInfo: GetResourceAuthInfoResponse | undefined;
|
||||||
|
try {
|
||||||
|
const res = await internal.get<
|
||||||
|
AxiosResponse<GetResourceAuthInfoResponse>
|
||||||
|
>(`/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<AxiosResponse<GetResourceResponse>>(
|
||||||
|
`/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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="p-3 md:mt-32">
|
<div className="w-full max-w-md mx-auto p-3 md:mt-32">
|
||||||
<ResourceAuthPortal />
|
<ResourceAuthPortal
|
||||||
|
methods={{
|
||||||
|
password: authInfo.password,
|
||||||
|
pincode: authInfo.pincode,
|
||||||
|
sso: authInfo.sso && !userIsUnauthorized,
|
||||||
|
}}
|
||||||
|
resource={{
|
||||||
|
name: authInfo.resourceName,
|
||||||
|
id: authInfo.resourceId,
|
||||||
|
}}
|
||||||
|
redirect={searchParams.r || authInfo.url}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -415,7 +415,7 @@ export default function ResourceAuthenticationPage() {
|
||||||
<section className="space-y-8">
|
<section className="space-y-8">
|
||||||
<SettingsSectionTitle
|
<SettingsSectionTitle
|
||||||
title="Authentication Methods"
|
title="Authentication Methods"
|
||||||
description="You can also anyone to access the resource via the below methods"
|
description="Allow anyone to access the resource via the below methods"
|
||||||
size="1xl"
|
size="1xl"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -39,66 +39,64 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="shadow-none">
|
<Alert>
|
||||||
<Alert>
|
<InfoIcon className="h-4 w-4" />
|
||||||
<InfoIcon className="h-4 w-4" />
|
<AlertTitle className="font-semibold">
|
||||||
<AlertTitle className="font-semibold">
|
Resource Information
|
||||||
Resource Information
|
</AlertTitle>
|
||||||
</AlertTitle>
|
<AlertDescription className="mt-3">
|
||||||
<AlertDescription className="mt-3">
|
<div className="space-y-3">
|
||||||
<div className="space-y-3">
|
<div>
|
||||||
<div>
|
{authInfo.password ||
|
||||||
{authInfo.password ||
|
authInfo.pincode ||
|
||||||
authInfo.pincode ||
|
authInfo.sso ? (
|
||||||
authInfo.sso ? (
|
<div className="flex items-center space-x-2 text-green-500">
|
||||||
<div className="flex items-center space-x-2 text-green-500">
|
<ShieldCheck />
|
||||||
<ShieldCheck />
|
<span>
|
||||||
<span>
|
This resource is protected with at least one
|
||||||
This resource is protected with at least
|
auth method.
|
||||||
one auth method.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center space-x-2 text-yellow-500">
|
|
||||||
<ShieldOff />
|
|
||||||
<span>
|
|
||||||
This resource is not protected with any
|
|
||||||
auth method. Anyone can access this
|
|
||||||
resource.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2 bg-muted p-1 pl-3 rounded-md">
|
|
||||||
<LinkIcon className="h-4 w-4" />
|
|
||||||
<a
|
|
||||||
href={fullUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-sm font-mono flex-grow hover:underline truncate"
|
|
||||||
>
|
|
||||||
{fullUrl}
|
|
||||||
</a>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={copyToClipboard}
|
|
||||||
className="ml-2"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{copied ? (
|
|
||||||
<CheckIcon className="h-4 w-4 text-green-500" />
|
|
||||||
) : (
|
|
||||||
<CopyIcon className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
<span className="ml-2">
|
|
||||||
{copied ? "Copied!" : "Copy"}
|
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
<div className="flex items-center space-x-2 text-yellow-500">
|
||||||
|
<ShieldOff />
|
||||||
|
<span>
|
||||||
|
This resource is not protected with any auth
|
||||||
|
method. Anyone can access this resource.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* <p className="mt-3">
|
<div className="flex items-center space-x-2 bg-muted p-1 pl-3 rounded-md">
|
||||||
|
<LinkIcon className="h-4 w-4" />
|
||||||
|
<a
|
||||||
|
href={fullUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-sm font-mono flex-grow hover:underline truncate"
|
||||||
|
>
|
||||||
|
{fullUrl}
|
||||||
|
</a>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
className="ml-2"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<CheckIcon className="h-4 w-4 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<CopyIcon className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
<span className="ml-2">
|
||||||
|
{copied ? "Copied!" : "Copy"}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <p className="mt-3">
|
||||||
To create a proxy to your private services,{" "}
|
To create a proxy to your private services,{" "}
|
||||||
<Link
|
<Link
|
||||||
href={`/${org.org.orgId}/settings/resources/${resource.resourceId}/connectivity`}
|
href={`/${org.org.orgId}/settings/resources/${resource.resourceId}/connectivity`}
|
||||||
|
@ -108,9 +106,8 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
to this resource
|
to this resource
|
||||||
</p> */}
|
</p> */}
|
||||||
</div>
|
</div>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,12 +99,12 @@ export default function InviteStatusCard({
|
||||||
<div className="p-3 md:mt-32 flex items-center justify-center">
|
<div className="p-3 md:mt-32 flex items-center justify-center">
|
||||||
<Card className="w-full max-w-md">
|
<Card className="w-full max-w-md">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center justify-center w-20 h-20 rounded-full bg-red-100 mx-auto mb-4">
|
{/* <div className="flex items-center justify-center w-20 h-20 rounded-full bg-red-100 mx-auto mb-4">
|
||||||
<XCircle
|
<XCircle
|
||||||
className="w-10 h-10 text-red-600"
|
className="w-10 h-10 text-red-600"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> */}
|
||||||
<CardTitle className="text-center text-2xl font-bold">
|
<CardTitle className="text-center text-2xl font-bold">
|
||||||
Invite Not Accepted
|
Invite Not Accepted
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const alertVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|
|
@ -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<
|
const Card = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
|
@ -9,13 +9,13 @@ const Card = React.forwardRef<
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
"rounded-lg border bg-card text-card-foreground",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
Card.displayName = "Card"
|
Card.displayName = "Card";
|
||||||
|
|
||||||
const CardHeader = React.forwardRef<
|
const CardHeader = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
|
@ -26,8 +26,8 @@ const CardHeader = React.forwardRef<
|
||||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
CardHeader.displayName = "CardHeader"
|
CardHeader.displayName = "CardHeader";
|
||||||
|
|
||||||
const CardTitle = React.forwardRef<
|
const CardTitle = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
|
@ -37,12 +37,12 @@ const CardTitle = React.forwardRef<
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-2xl font-semibold leading-none tracking-tight",
|
"text-2xl font-semibold leading-none tracking-tight",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
CardTitle.displayName = "CardTitle"
|
CardTitle.displayName = "CardTitle";
|
||||||
|
|
||||||
const CardDescription = React.forwardRef<
|
const CardDescription = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
|
@ -53,16 +53,16 @@ const CardDescription = React.forwardRef<
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
CardDescription.displayName = "CardDescription"
|
CardDescription.displayName = "CardDescription";
|
||||||
|
|
||||||
const CardContent = React.forwardRef<
|
const CardContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
))
|
));
|
||||||
CardContent.displayName = "CardContent"
|
CardContent.displayName = "CardContent";
|
||||||
|
|
||||||
const CardFooter = React.forwardRef<
|
const CardFooter = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
|
@ -73,7 +73,14 @@ const CardFooter = React.forwardRef<
|
||||||
className={cn("flex items-center p-6 pt-0", className)}
|
className={cn("flex items-center p-6 pt-0", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
CardFooter.displayName = "CardFooter"
|
CardFooter.displayName = "CardFooter";
|
||||||
|
|
||||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
};
|
||||||
|
|
|
@ -29,7 +29,7 @@ const TabsTrigger = React.forwardRef<
|
||||||
<TabsPrimitive.Trigger
|
<TabsPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue