Self-hosted dashboard and admin auth (#4837)

Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
This commit is contained in:
Gabriel Stein
2026-04-23 19:06:36 +05:30
committed by GitHub
co-authored by kartik-mem0
parent 15feaa8ac4
commit db8ac61713
252 changed files with 20275 additions and 331 deletions
+135
View File
@@ -0,0 +1,135 @@
"use client";
import {
createContext,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import { api, setAccessToken } from "@/utils/api";
import { AUTH_ENDPOINTS } from "@/utils/api-endpoints";
export interface AuthUser {
id: string;
name: string;
email: string;
role: string;
created_at: string;
}
interface AuthContextValue {
user: AuthUser | null;
isLoading: boolean;
isAdmin: boolean;
login: (email: string, password: string) => Promise<void>;
register: (name: string, email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
refreshUser: () => Promise<void>;
}
export const AuthContext = createContext<AuthContextValue>({
user: null,
isLoading: true,
isAdmin: false,
login: async () => {},
register: async () => {},
logout: async () => {},
refreshUser: async () => {},
});
async function storeRefreshToken(refreshToken: string) {
await fetch("/api/auth/refresh", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refresh_token: refreshToken }),
});
}
async function clearRefreshToken() {
await fetch("/api/auth/refresh", { method: "DELETE" });
}
async function refreshSession(): Promise<boolean> {
const res = await fetch("/api/auth/refresh", {
method: "POST",
credentials: "include",
});
if (!res.ok) return false;
const data = await res.json();
setAccessToken(data.access_token);
return true;
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<AuthUser | null>(null);
const [isLoading, setIsLoading] = useState(true);
const loadUser = useCallback(async () => {
const res = await api.get<AuthUser>(AUTH_ENDPOINTS.ME);
setUser(res.data);
}, []);
useEffect(() => {
let active = true;
(async () => {
try {
const ok = await refreshSession();
if (ok && active) await loadUser();
} catch {
if (active) setUser(null);
} finally {
if (active) setIsLoading(false);
}
})();
return () => {
active = false;
};
}, [loadUser]);
const login = useCallback(
async (email: string, password: string) => {
const res = await api.post(AUTH_ENDPOINTS.LOGIN, { email, password });
setAccessToken(res.data.access_token);
await storeRefreshToken(res.data.refresh_token);
await loadUser();
},
[loadUser],
);
const register = useCallback(
async (name: string, email: string, password: string) => {
const res = await api.post(AUTH_ENDPOINTS.REGISTER, {
name,
email,
password,
});
setAccessToken(res.data.access_token);
await storeRefreshToken(res.data.refresh_token);
await loadUser();
},
[loadUser],
);
const logout = useCallback(async () => {
await clearRefreshToken();
setAccessToken(null);
setUser(null);
if (typeof window !== "undefined") window.location.href = "/login";
}, []);
const value = useMemo<AuthContextValue>(
() => ({
user,
isLoading,
isAdmin: user?.role === "admin",
login,
register,
logout,
refreshUser: loadUser,
}),
[user, isLoading, login, register, logout, loadUser],
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
+20
View File
@@ -0,0 +1,20 @@
import axios from "axios";
export function getErrorMessage(
err: unknown,
fallback = "Something went wrong",
): string {
if (axios.isAxiosError(err)) {
const detail = err.response?.data?.detail;
if (typeof detail === "string") return detail;
if (Array.isArray(detail) && detail.length > 0) {
const first = detail[0];
if (typeof first === "string") return first;
if (first?.msg) return String(first.msg);
}
if (err.message) return err.message;
}
if (err instanceof Error && err.message) return err.message;
if (typeof err === "string") return err;
return fallback;
}
@@ -0,0 +1,7 @@
export function getServerApiUrl(): string {
return (
process.env.API_INTERNAL_URL ||
process.env.NEXT_PUBLIC_API_URL ||
"http://localhost:8888"
);
}
+14
View File
@@ -0,0 +1,14 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function formatCompactNumber(value: number): string {
if (!Number.isFinite(value)) return "0";
return new Intl.NumberFormat("en", {
notation: "compact",
maximumFractionDigits: 1,
}).format(value);
}
+3
View File
@@ -0,0 +1,3 @@
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export const isValidEmail = (value: string) => EMAIL_RE.test(value.trim());