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 (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
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}
+
+ );
+}