Skip to content

Commit

Permalink
add outerbase email and password login (#326)
Browse files Browse the repository at this point in the history
* add outerbase email and password login

* polish the design
  • Loading branch information
invisal authored Jan 31, 2025
1 parent 735eac6 commit 075cdd0
Show file tree
Hide file tree
Showing 11 changed files with 340 additions and 3 deletions.
Binary file added public/assets/login-planet.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/login-portal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/login-stars.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions src/app/(theme)/w/[workspaceId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { OuterbaseSessionProvider } from "@/outerbase-cloud/session-provider";

export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return <OuterbaseSessionProvider>{children}</OuterbaseSessionProvider>;
}
99 changes: 99 additions & 0 deletions src/app/signin/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<body className="dark">
<div
className="absolute left-[10%] z-2 flex w-[400px] flex-col gap-4 rounded-lg border-neutral-800 bg-neutral-900 p-8 md:m-0"
style={{
top: "50%",
transform: "translateY(-50%)",
}}
>
<div className="mb-8 flex flex-col items-center text-white">
<svg
fill="currentColor"
viewBox="75 75 350 350"
className="mb-2 h-14 w-14 text-black dark:text-white"
>
<path d="M249.51,146.58c-58.7,0-106.45,49.37-106.45,110.04c0,60.68,47.76,110.04,106.45,110.04 c58.7,0,106.46-49.37,106.46-110.04C355.97,195.95,308.21,146.58,249.51,146.58z M289.08,332.41l-0.02,0.04l-0.51,0.65 c-5.55,7.06-12.37,9.35-17.11,10.02c-1.23,0.17-2.5,0.26-3.78,0.26c-12.94,0-25.96-9.09-37.67-26.29 c-9.56-14.05-17.84-32.77-23.32-52.71c-9.78-35.61-8.67-68.08,2.83-82.74c5.56-7.07,12.37-9.35,17.11-10.02 c13.46-1.88,27.16,6.2,39.64,23.41c10.29,14.19,19.22,33.83,25.12,55.32C301,285.35,300.08,317.46,289.08,332.41z"></path>
</svg>

<h1 className="text-2xl font-bold">Welcome back</h1>
<p>Sign in to your existing account</p>
</div>

<LabelInput
label="Work Email"
value={email}
placeholder="Enter your email address"
onChange={(e) => setEmail(e.currentTarget.value)}
/>

<LabelInput
label="Password"
value={password}
type="password"
placeholder="Password"
onChange={(e) => setPassword(e.currentTarget.value)}
/>

{error && <div className="text-red-400">{error}</div>}

<Button onClick={onLoginClicked}>
{loading && <LucideLoader className="mr-1 h-4 w-4 animate-spin" />}
Continue with email
</Button>

<Link href="#" className="text-sm">
Forget password
</Link>
</div>

<LoginBaseSpaceship />
</body>
);
}
48 changes: 48 additions & 0 deletions src/app/signin/starbase-portal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import "./styles.css";

export function LoginBaseSpaceship() {
return (
<div
className="absolute top-0 bottom-0 opacity-40 md:opacity-100"
style={{
left: "50%",
height: "100vh",
width: "100vh",
transform: "translateX(-50%)",
}}
>
<div
className="absolute z-1 flex h-full w-full overflow-hidden bg-no-repeat"
style={{
backgroundImage: `url("/assets/login-portal.png")`,
backgroundSize: "contain",
backgroundPosition: "center",
backgroundColor: "transparent",
}}
></div>

<div
className="absolute left-1 z-0 h-full w-full"
style={{ width: "calc(100% - 8px)" }}
>
<div
className="absolute h-full w-full"
style={{ backgroundColor: "#0d1013" }}
>
<img
src="/assets/login-stars.png"
alt="stars"
className="stars-animation absolute w-full"
/>

<img
src="/assets/login-planet.png"
alt="planet"
className="planet-animation absolute w-full"
style={{ bottom: "16%" }}
/>
</div>
</div>
</div>
);
}
53 changes: 53 additions & 0 deletions src/app/signin/styles.css
Original file line number Diff line number Diff line change
@@ -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;
}
11 changes: 11 additions & 0 deletions src/components/label-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Input, InputProps } from "./ui/input";
import { Label } from "./ui/label";

export default function LabelInput(props: InputProps & { label: string }) {
return (
<div className="flex flex-col gap-2">
<Label>{props.label}</Label>
<Input {...props} />
</div>
);
}
44 changes: 44 additions & 0 deletions src/outerbase-cloud/api-type.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,36 @@

export interface OuterbaseDatabaseConfig {
token: string;
workspaceId: string;
baseId: string;
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<T = unknown> {
success: boolean;
response: T;
error?: OuterbaseAPIErrorResponse
}

export interface OuterbaseAPIQueryRaw {
Expand Down Expand Up @@ -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[];
}
Expand Down
24 changes: 21 additions & 3 deletions src/outerbase-cloud/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import {
OuterbaseAPIBaseResponse,
OuterbaseAPIDashboardDetail,
OuterbaseAPIDashboardListResponse,
OuterbaseAPIError,
OuterbaseAPIQuery,
OuterbaseAPIQueryListResponse,
OuterbaseAPIQueryRaw,
OuterbaseAPIResponse,
OuterbaseAPISession,
OuterbaseAPIUser,
OuterbaseAPIWorkspaceResponse,
} from "./api-type";

Expand All @@ -24,6 +27,11 @@ export async function requestOuterbase<T = unknown>(
});

const json = (await raw.json()) as OuterbaseAPIResponse<T>;

if (json.error) {
throw new OuterbaseAPIError(json.error)
}

return json.response;
}

Expand All @@ -34,9 +42,9 @@ export function getOuterbaseWorkspace() {
export async function getOuterbaseBase(workspaceId: string, baseId: string) {
const baseList = await requestOuterbase<OuterbaseAPIBaseResponse>(
"/api/v1/workspace/" +
workspaceId +
"/connection?" +
new URLSearchParams({ baseId })
workspaceId +
"/connection?" +
new URLSearchParams({ baseId })
);

return baseList.items[0];
Expand Down Expand Up @@ -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<OuterbaseAPISession>("/api/v1/auth/login", "POST", {
email, password
})
}
55 changes: 55 additions & 0 deletions src/outerbase-cloud/session-provider.tsx
Original file line number Diff line number Diff line change
@@ -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<OuterbaseAPISession>();
const [user, setUser] = useState<OuterbaseAPIUser>();

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 <div>Loading...</div>;
}

if (!session || !user) {
return <div>Something wrong!</div>;
}

return (
<OuterbaseSessionContext.Provider value={{ session, user }}>
{children}
</OuterbaseSessionContext.Provider>
);
}

0 comments on commit 075cdd0

Please sign in to comment.