diff --git a/public/assets/login-planet.png b/public/assets/login-planet.png new file mode 100644 index 00000000..d6b1e9db Binary files /dev/null and b/public/assets/login-planet.png differ diff --git a/public/assets/login-portal.png b/public/assets/login-portal.png new file mode 100644 index 00000000..ea1c0562 Binary files /dev/null and b/public/assets/login-portal.png differ diff --git a/public/assets/login-stars.png b/public/assets/login-stars.png new file mode 100644 index 00000000..00143153 Binary files /dev/null and b/public/assets/login-stars.png differ diff --git a/src/app/(theme)/w/[workspaceId]/layout.tsx b/src/app/(theme)/w/[workspaceId]/layout.tsx new file mode 100644 index 00000000..ef584ba7 --- /dev/null +++ b/src/app/(theme)/w/[workspaceId]/layout.tsx @@ -0,0 +1,9 @@ +import { OuterbaseSessionProvider } from "@/outerbase-cloud/session-provider"; + +export default async function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return {children}; +} diff --git a/src/app/signin/page.tsx b/src/app/signin/page.tsx new file mode 100644 index 00000000..24bb9109 --- /dev/null +++ b/src/app/signin/page.tsx @@ -0,0 +1,99 @@ +"use client"; +import LabelInput from "@/components/label-input"; +import { Button } from "@/components/ui/button"; +import { + getOuterbaseWorkspace, + loginOuterbaseByPassword, +} from "@/outerbase-cloud/api"; +import { OuterbaseAPIError } from "@/outerbase-cloud/api-type"; +import { LucideLoader } from "lucide-react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useCallback, useState } from "react"; +import { LoginBaseSpaceship } from "./starbase-portal"; + +export default function SigninPage() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const router = useRouter(); + const [loading, setLoading] = useState(false); + + const onLoginClicked = useCallback(() => { + setLoading(true); + + loginOuterbaseByPassword(email, password) + .then((session) => { + localStorage.setItem("session", JSON.stringify(session)); + localStorage.setItem("ob-token", session.token); + + getOuterbaseWorkspace() + .then((w) => { + router.push(`/w/${w.items[0].short_name}`); + }) + .catch(console.error) + .finally(() => { + setLoading(false); + }); + }) + .catch((e) => { + setLoading(false); + if (e instanceof OuterbaseAPIError) { + setError(e.description); + } + }); + }, [email, password, router]); + + return ( + +
+
+ + + + +

Welcome back

+

Sign in to your existing account

+
+ + setEmail(e.currentTarget.value)} + /> + + setPassword(e.currentTarget.value)} + /> + + {error &&
{error}
} + + + + + Forget password + +
+ + + + ); +} diff --git a/src/app/signin/starbase-portal.tsx b/src/app/signin/starbase-portal.tsx new file mode 100644 index 00000000..9ad41526 --- /dev/null +++ b/src/app/signin/starbase-portal.tsx @@ -0,0 +1,48 @@ +import "./styles.css"; + +export function LoginBaseSpaceship() { + return ( +
+
+ +
+
+ stars + + planet +
+
+
+ ); +} diff --git a/src/app/signin/styles.css b/src/app/signin/styles.css new file mode 100644 index 00000000..0c507818 --- /dev/null +++ b/src/app/signin/styles.css @@ -0,0 +1,53 @@ +@keyframes moveStars { + 0% { + bottom: 32%; + transform: rotate(0deg); + } + 25% { + bottom: 33%; + transform: rotate(-1deg); + } + 50% { + bottom: 34%; + transform: rotate(0deg); + } + 75% { + bottom: 33%; + transform: rotate(1deg); + } + 100% { + bottom: 32%; + transform: rotate(0deg); + } +} + +@keyframes movePlanet { + 0% { + bottom: 16%; + transform: rotate(0deg); + } + 25% { + bottom: 17.5%; + transform: rotate(1deg); + } + 50% { + bottom: 20%; + transform: rotate(0deg); + } + 75% { + bottom: 17.5%; + transform: rotate(-1deg); + } + 100% { + bottom: 16%; + transform: rotate(0deg); + } +} + +.stars-animation { + animation: moveStars 30s ease-in-out infinite; +} + +.planet-animation { + animation: movePlanet 30s ease-in-out infinite; +} diff --git a/src/components/label-input.tsx b/src/components/label-input.tsx new file mode 100644 index 00000000..aa96e147 --- /dev/null +++ b/src/components/label-input.tsx @@ -0,0 +1,11 @@ +import { Input, InputProps } from "./ui/input"; +import { Label } from "./ui/label"; + +export default function LabelInput(props: InputProps & { label: string }) { + return ( +
+ + +
+ ); +} diff --git a/src/outerbase-cloud/api-type.ts b/src/outerbase-cloud/api-type.ts index 0655cfe6..269c6a31 100644 --- a/src/outerbase-cloud/api-type.ts +++ b/src/outerbase-cloud/api-type.ts @@ -1,3 +1,4 @@ + export interface OuterbaseDatabaseConfig { token: string; workspaceId: string; @@ -5,9 +6,31 @@ export interface OuterbaseDatabaseConfig { sourceId: string; } +export class OuterbaseAPIError extends Error { + public readonly description: string; + public readonly code: string; + public readonly title: string; + + constructor(error: OuterbaseAPIErrorResponse) { + super(error.description); + + this.description = error.description; + this.code = error.code; + this.message = error.description; + this.title = error.title; + } +} + +export interface OuterbaseAPIErrorResponse { + code: string, + description: string, + title: string +} + export interface OuterbaseAPIResponse { success: boolean; response: T; + error?: OuterbaseAPIErrorResponse } export interface OuterbaseAPIQueryRaw { @@ -98,6 +121,27 @@ export interface OuterbaseAPIDashboardChart { workspace_id: string; } +export interface OuterbaseAPISession { + created_at: string; + user_id: string; + phone_verified_at: string | null; + password_verified_at: string | null; + otp_verified_at: string | null; + oauth_verified_at: string | null; + expires_at: string | null; + token: string; +} + +export interface OuterbaseAPIUser { + avatar: string | null; + google_user_id: string; + initials: string; + id: string; + email: string; + last_name: string; + first_name: string; +} + export interface OuterbaseAPIDashboardDetail extends OuterbaseAPIDashboard { charts: OuterbaseAPIDashboardChart[]; } diff --git a/src/outerbase-cloud/api.ts b/src/outerbase-cloud/api.ts index 9a3ad62b..ef1f1b01 100644 --- a/src/outerbase-cloud/api.ts +++ b/src/outerbase-cloud/api.ts @@ -2,10 +2,13 @@ import { OuterbaseAPIBaseResponse, OuterbaseAPIDashboardDetail, OuterbaseAPIDashboardListResponse, + OuterbaseAPIError, OuterbaseAPIQuery, OuterbaseAPIQueryListResponse, OuterbaseAPIQueryRaw, OuterbaseAPIResponse, + OuterbaseAPISession, + OuterbaseAPIUser, OuterbaseAPIWorkspaceResponse, } from "./api-type"; @@ -24,6 +27,11 @@ export async function requestOuterbase( }); const json = (await raw.json()) as OuterbaseAPIResponse; + + if (json.error) { + throw new OuterbaseAPIError(json.error) + } + return json.response; } @@ -34,9 +42,9 @@ export function getOuterbaseWorkspace() { export async function getOuterbaseBase(workspaceId: string, baseId: string) { const baseList = await requestOuterbase( "/api/v1/workspace/" + - workspaceId + - "/connection?" + - new URLSearchParams({ baseId }) + workspaceId + + "/connection?" + + new URLSearchParams({ baseId }) ); return baseList.items[0]; @@ -111,3 +119,13 @@ export async function updateOuterbaseQuery( options ); } + +export async function getOuterbaseSession() { + return requestOuterbase<{ session: OuterbaseAPISession, user: OuterbaseAPIUser }>('/api/v1/auth/session') +} + +export async function loginOuterbaseByPassword(email: string, password: string) { + return requestOuterbase("/api/v1/auth/login", "POST", { + email, password + }) +} \ No newline at end of file diff --git a/src/outerbase-cloud/session-provider.tsx b/src/outerbase-cloud/session-provider.tsx new file mode 100644 index 00000000..d32231ab --- /dev/null +++ b/src/outerbase-cloud/session-provider.tsx @@ -0,0 +1,55 @@ +"use client"; +import { useRouter } from "next/navigation"; +import { createContext, PropsWithChildren, useEffect, useState } from "react"; +import { getOuterbaseSession } from "./api"; +import { OuterbaseAPISession, OuterbaseAPIUser } from "./api-type"; + +interface OuterebaseSessionContextProps { + session: OuterbaseAPISession; + user: OuterbaseAPIUser; +} + +const OuterbaseSessionContext = createContext<{ + session: OuterbaseAPISession; + user: OuterbaseAPIUser; +}>({} as OuterebaseSessionContextProps); + +export function OuterbaseSessionProvider({ children }: PropsWithChildren) { + const router = useRouter(); + const [loading, setLoading] = useState(true); + const [session, setSession] = useState(); + const [user, setUser] = useState(); + + useEffect(() => { + if (typeof window === "undefined") return; + + const token = localStorage.getItem("ob-token"); + if (!token) return; + + getOuterbaseSession() + .then((r) => { + setSession(r.session); + setUser(r.user); + }) + .catch(() => { + router.push("/signin"); + }) + .finally(() => { + setLoading(false); + }); + }, [router]); + + if (loading) { + return
Loading...
; + } + + if (!session || !user) { + return
Something wrong!
; + } + + return ( + + {children} + + ); +}