Self-hosted dashboard and admin auth (#4837)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
This commit is contained in:
co-authored by
kartik-mem0
parent
15feaa8ac4
commit
db8ac61713
@@ -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>;
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
export const isValidEmail = (value: string) => EMAIL_RE.test(value.trim());
|
||||
Reference in New Issue
Block a user