diff --git a/public/images/auth-page-bg-strip.svg b/public/images/auth-page-bg-strip.svg new file mode 100644 index 0000000000..83984675e3 --- /dev/null +++ b/public/images/auth-page-bg-strip.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/AuthPageBase.tsx b/src/components/AuthPageBase.tsx new file mode 100644 index 0000000000..93bf9eb743 --- /dev/null +++ b/src/components/AuthPageBase.tsx @@ -0,0 +1,65 @@ +import { Logo } from "@instill-ai/design-system"; +import Image from "next/image"; + +export const AuthPageBase = ({ children }: { children: React.ReactNode }) => { + return
{children}
; +}; + +const Container = ({ children }: { children: React.ReactNode }) => { + return
{children}
; +}; + +const Content = ({ children }: { children: React.ReactNode }) => { + return ( +
+
+
+
+ +
+
+

+ Meet{" "} + + Instill Cloud + +

+

+ The Backbone for All Your AI Needs +

+
+

+ A no-code/low-code platform to build AI-first applications to + process your text, image, video and audio in minutes +

+
+
+ +
+

+ © Instill AI 2023 +

+ auth-page-bg-strip +
+
+
+
+
+ {children} +
+
+
+ ); +}; + +AuthPageBase.Container = Container; +AuthPageBase.Content = Content; diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx new file mode 100644 index 0000000000..02f4747bfd --- /dev/null +++ b/src/components/LoginForm.tsx @@ -0,0 +1,78 @@ +import { Button, Form, Input } from "@instill-ai/design-system"; +import * as z from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; + +const LoginFormSchema = z.object({ + username: z.string(), + password: z.string(), +}); + +export const LoginForm = () => { + const form = useForm>({ + resolver: zodResolver(LoginFormSchema), + }); + + function onSubmit(data: z.infer) { + alert(JSON.stringify(data)); + } + + return ( + +
+
+ { + return ( + + Username + + + + + + + + ); + }} + /> + { + return ( + + Password + + + + + + + + ); + }} + /> +
+ +
+
+ ); +}; diff --git a/src/components/index.ts b/src/components/index.ts index 5b98ccf670..1ca842d8f1 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,5 +1,7 @@ +export * from "./AuthPageBase"; export * from "./ConsoleCorePageHead"; export * from "./Sidebar"; export * from "./ErrorBoundary"; +export * from "./LoginForm"; export * from "./ModelReadmeMarkdown"; export * from "./OnboardingForm"; diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts new file mode 100644 index 0000000000..0c92cb7c95 --- /dev/null +++ b/src/lib/auth/index.ts @@ -0,0 +1 @@ +export * from "./withMiddlewareAuthRequired"; diff --git a/src/lib/auth/withMiddlewareAuthRequired.ts b/src/lib/auth/withMiddlewareAuthRequired.ts new file mode 100644 index 0000000000..faf40016eb --- /dev/null +++ b/src/lib/auth/withMiddlewareAuthRequired.ts @@ -0,0 +1,67 @@ +import { NextMiddleware, NextResponse } from "next/server"; + +export type WithMiddlewareAuthRequiredOptions = { + middleware?: NextMiddleware; +}; + +export function withMiddlewareAuthRequired( + props?: NextMiddleware | WithMiddlewareAuthRequiredOptions +): NextMiddleware { + return async function wrappedMiddleware(...args) { + const [req] = args; + let middleware: NextMiddleware | undefined; + const { pathname, origin } = req.nextUrl; + + if (typeof props === "function") { + middleware = props; + } else if (props?.middleware) { + middleware = props.middleware; + } + + const ignorePaths = ["/_next", "/favicon.ico"]; + if (ignorePaths.some((p) => pathname.startsWith(p))) { + return; + } + + const authRes = NextResponse.next(); + const sessionCookie = req.cookies.get("instill-ai-session"); + const session = JSON.parse(sessionCookie?.value || "{}"); + + if (!session.access_token) { + if (pathname.startsWith("/api")) { + return NextResponse.json( + { + error: "not_authenticated", + description: + "The user does not have an active session or is not authenticated", + }, + { status: 401 } + ); + } + return NextResponse.redirect(new URL("/login", origin)); + } + + const providedMiddlewareRes = await (middleware && middleware(...args)); + + if (providedMiddlewareRes) { + const nextRes = new NextResponse( + providedMiddlewareRes.body, + providedMiddlewareRes + ); + const cookies = authRes.cookies.getAll(); + if ("cookies" in providedMiddlewareRes) { + for (const cookie of providedMiddlewareRes.cookies.getAll()) { + nextRes.cookies.set(cookie); + } + } + for (const cookie of cookies) { + if (!nextRes.cookies.get(cookie.name)) { + nextRes.cookies.set(cookie); + } + } + return nextRes; + } else { + return authRes; + } + }; +} diff --git a/src/lib/index.ts b/src/lib/index.ts index 852b0a5577..b6d432a3b5 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,2 +1,3 @@ +export * from "./auth"; export * from "./mgmtRoleOptions"; export * from "./useTrackingToken"; diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000000..49aa953454 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,13 @@ +import { withMiddlewareAuthRequired } from "./lib"; + +export default withMiddlewareAuthRequired(); + +export const config = { + matcher: [ + "/resources/:path*", + "/dashboard/:path*", + "/model-hub/:path*", + "/pipelines/:path*", + "/settings/:path*", + ], +}; diff --git a/src/pages/login.tsx b/src/pages/login.tsx new file mode 100644 index 0000000000..2e68dce9d8 --- /dev/null +++ b/src/pages/login.tsx @@ -0,0 +1,25 @@ +import { NextPageWithLayout } from "./_app"; +import { AuthPageBase, LoginForm } from "@/components"; + +const LoginPage: NextPageWithLayout = () => { + return ( +
+

+ Login +

+ +
+ ); +}; + +LoginPage.getLayout = (page) => { + return ( + + + {page} + + + ); +}; + +export default LoginPage;