Add change password to profile, persist all profile fields to DB
- Add jobTitle, company, bio columns to User model (previously localStorage only) - Replace legacy Authentik GET /api/auth/me with session-based profile endpoint - Update PUT /api/auth/me to persist all profile fields to DB - Add PUT /api/auth/change-password endpoint with current password verification - Add Security card to profile page with change password form - Update user-profile provider to fetch from API instead of localStorage Made-with: Cursor
This commit is contained in:
parent
028840a887
commit
a83c41694f
5 changed files with 287 additions and 104 deletions
|
|
@ -17,6 +17,9 @@ model User {
|
||||||
username String?
|
username String?
|
||||||
displayName String?
|
displayName String?
|
||||||
avatarUrl String @default("")
|
avatarUrl String @default("")
|
||||||
|
jobTitle String @default("")
|
||||||
|
company String @default("")
|
||||||
|
bio String @default("")
|
||||||
role String @default("viewer")
|
role String @default("viewer")
|
||||||
activeOrgId String?
|
activeOrgId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@ import {
|
||||||
Building2,
|
Building2,
|
||||||
Shield,
|
Shield,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
Lock,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { Header } from "@/components/layout/header";
|
import { Header } from "@/components/layout/header";
|
||||||
|
|
@ -60,6 +63,47 @@ export default function ProfilePage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const [saving, setSaving] = React.useState(false);
|
const [saving, setSaving] = React.useState(false);
|
||||||
|
const [pwForm, setPwForm] = React.useState({
|
||||||
|
currentPassword: "",
|
||||||
|
newPassword: "",
|
||||||
|
confirmPassword: "",
|
||||||
|
});
|
||||||
|
const [pwSaving, setPwSaving] = React.useState(false);
|
||||||
|
const [showCurrentPw, setShowCurrentPw] = React.useState(false);
|
||||||
|
const [showNewPw, setShowNewPw] = React.useState(false);
|
||||||
|
|
||||||
|
const handlePasswordChange = async () => {
|
||||||
|
if (pwForm.newPassword.length < 8) {
|
||||||
|
toast.error("Password must be at least 8 characters");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pwForm.newPassword !== pwForm.confirmPassword) {
|
||||||
|
toast.error("Passwords do not match");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPwSaving(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/auth/change-password", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
currentPassword: pwForm.currentPassword,
|
||||||
|
newPassword: pwForm.newPassword,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) {
|
||||||
|
toast.error(data.error || "Failed to change password");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.success("Password changed successfully");
|
||||||
|
setPwForm({ currentPassword: "", newPassword: "", confirmPassword: "" });
|
||||||
|
} catch {
|
||||||
|
toast.error("Failed to change password");
|
||||||
|
} finally {
|
||||||
|
setPwSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
|
|
@ -264,6 +308,117 @@ export default function ProfilePage() {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Security — Change Password */}
|
||||||
|
<Card variant="glass">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
|
<Lock className="size-4" />
|
||||||
|
Security
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{profile.hasPassword
|
||||||
|
? "Change your account password"
|
||||||
|
: "Set a password for your account"}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{profile.hasPassword && (
|
||||||
|
<div>
|
||||||
|
<Label className="mb-1.5 block text-xs font-medium text-muted-foreground">
|
||||||
|
Current Password
|
||||||
|
</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
type={showCurrentPw ? "text" : "password"}
|
||||||
|
value={pwForm.currentPassword}
|
||||||
|
onChange={(e) =>
|
||||||
|
setPwForm((p) => ({ ...p, currentPassword: e.target.value }))
|
||||||
|
}
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||||
|
onClick={() => setShowCurrentPw(!showCurrentPw)}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{showCurrentPw ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<Label className="mb-1.5 block text-xs font-medium text-muted-foreground">
|
||||||
|
New Password
|
||||||
|
</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
type={showNewPw ? "text" : "password"}
|
||||||
|
value={pwForm.newPassword}
|
||||||
|
onChange={(e) =>
|
||||||
|
setPwForm((p) => ({ ...p, newPassword: e.target.value }))
|
||||||
|
}
|
||||||
|
placeholder="At least 8 characters"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||||
|
onClick={() => setShowNewPw(!showNewPw)}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{showNewPw ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="mb-1.5 block text-xs font-medium text-muted-foreground">
|
||||||
|
Confirm New Password
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={pwForm.confirmPassword}
|
||||||
|
onChange={(e) =>
|
||||||
|
setPwForm((p) => ({ ...p, confirmPassword: e.target.value }))
|
||||||
|
}
|
||||||
|
placeholder="Repeat new password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pwForm.newPassword.length > 0 && pwForm.newPassword.length < 8 && (
|
||||||
|
<p className="text-xs text-amber-600 dark:text-amber-400">
|
||||||
|
Password must be at least 8 characters
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{pwForm.confirmPassword.length > 0 &&
|
||||||
|
pwForm.newPassword !== pwForm.confirmPassword && (
|
||||||
|
<p className="text-xs text-red-600 dark:text-red-400">
|
||||||
|
Passwords do not match
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="rounded-xl"
|
||||||
|
onClick={handlePasswordChange}
|
||||||
|
disabled={
|
||||||
|
pwSaving ||
|
||||||
|
pwForm.newPassword.length < 8 ||
|
||||||
|
pwForm.newPassword !== pwForm.confirmPassword ||
|
||||||
|
(profile.hasPassword && !pwForm.currentPassword)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{pwSaving ? (
|
||||||
|
<Loader2 className="mr-2 size-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Lock className="mr-2 size-4" />
|
||||||
|
)}
|
||||||
|
{profile.hasPassword ? "Change Password" : "Set Password"}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
60
src/app/api/auth/change-password/route.ts
Normal file
60
src/app/api/auth/change-password/route.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import bcrypt from "bcryptjs";
|
||||||
|
import { auth } from "@/auth";
|
||||||
|
import { prisma } from "@/lib/db";
|
||||||
|
|
||||||
|
export async function PUT(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { currentPassword, newPassword } = await req.json();
|
||||||
|
|
||||||
|
if (!newPassword || typeof newPassword !== "string" || newPassword.length < 8) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "New password must be at least 8 characters" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: session.user.id },
|
||||||
|
select: { hashedPassword: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: "User not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.hashedPassword) {
|
||||||
|
if (!currentPassword) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Current password is required" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const valid = await bcrypt.compare(currentPassword, user.hashedPassword);
|
||||||
|
if (!valid) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Current password is incorrect" },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(newPassword, 12);
|
||||||
|
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: session.user.id },
|
||||||
|
data: { hashedPassword },
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ ok: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[change-password] Error:", error);
|
||||||
|
return NextResponse.json({ error: "Something went wrong" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,80 +1,41 @@
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { prisma } from "@/lib/db";
|
import { prisma } from "@/lib/db";
|
||||||
import { getOrCreateUser, type AppUser } from "@/lib/auth";
|
|
||||||
|
|
||||||
export type AuthentikUser = {
|
export async function GET() {
|
||||||
username: string;
|
const session = await auth();
|
||||||
name: string;
|
if (!session?.user?.id) {
|
||||||
email: string;
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
groups: string[];
|
|
||||||
uid: string;
|
|
||||||
avatar: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type { AppUser };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads Authentik forward-auth headers injected by Traefik and optionally
|
|
||||||
* enriches with avatar from the Authentik API.
|
|
||||||
*
|
|
||||||
* Headers set by authentik forward-auth:
|
|
||||||
* X-authentik-username, X-authentik-name, X-authentik-email,
|
|
||||||
* X-authentik-groups, X-authentik-uid
|
|
||||||
*/
|
|
||||||
export async function GET(req: NextRequest) {
|
|
||||||
const username = req.headers.get("x-authentik-username") ?? "";
|
|
||||||
const name = req.headers.get("x-authentik-name") ?? "";
|
|
||||||
const email = req.headers.get("x-authentik-email") ?? "";
|
|
||||||
const groups = req.headers.get("x-authentik-groups") ?? "";
|
|
||||||
const uid = req.headers.get("x-authentik-uid") ?? "";
|
|
||||||
|
|
||||||
if (!username && !email) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ authenticated: false, user: null },
|
|
||||||
{ status: 200 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const user: AuthentikUser = {
|
const user = await prisma.user.findUnique({
|
||||||
username,
|
where: { id: session.user.id },
|
||||||
name,
|
select: {
|
||||||
email,
|
id: true,
|
||||||
groups: groups ? groups.split("|") : [],
|
email: true,
|
||||||
uid,
|
displayName: true,
|
||||||
avatar: "",
|
avatarUrl: true,
|
||||||
};
|
jobTitle: true,
|
||||||
|
company: true,
|
||||||
|
bio: true,
|
||||||
|
hashedPassword: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const authentikUrl = process.env.AUTHENTIK_URL;
|
if (!user) {
|
||||||
const authentikToken = process.env.AUTHENTIK_API_TOKEN;
|
return NextResponse.json({ error: "User not found" }, { status: 404 });
|
||||||
|
|
||||||
if (authentikUrl && authentikToken && uid) {
|
|
||||||
try {
|
|
||||||
const res = await fetch(
|
|
||||||
`${authentikUrl}/api/v3/core/users/?search=${encodeURIComponent(username)}&page_size=1`,
|
|
||||||
{
|
|
||||||
headers: { Authorization: `Bearer ${authentikToken}` },
|
|
||||||
signal: AbortSignal.timeout(5000),
|
|
||||||
next: { revalidate: 300 },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json();
|
|
||||||
const matchedUser = data.results?.[0];
|
|
||||||
if (matchedUser) {
|
|
||||||
user.avatar = matchedUser.avatar ?? "";
|
|
||||||
if (!user.name && matchedUser.name) user.name = matchedUser.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Authentik API unavailable — headers still provide the essentials
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dbUser = await getOrCreateUser(req.headers);
|
return NextResponse.json({
|
||||||
|
id: user.id,
|
||||||
return NextResponse.json({ authenticated: true, user, dbUser });
|
email: user.email,
|
||||||
|
displayName: user.displayName ?? "",
|
||||||
|
avatarUrl: user.avatarUrl ?? "",
|
||||||
|
jobTitle: user.jobTitle ?? "",
|
||||||
|
company: user.company ?? "",
|
||||||
|
bio: user.bio ?? "",
|
||||||
|
hasPassword: !!user.hashedPassword,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function PUT(req: NextRequest) {
|
export async function PUT(req: NextRequest) {
|
||||||
|
|
@ -83,15 +44,18 @@ export async function PUT(req: NextRequest) {
|
||||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { displayName, avatarUrl } = await req.json();
|
const body = await req.json();
|
||||||
const data: Record<string, string> = {};
|
const data: Record<string, string> = {};
|
||||||
if (typeof displayName === "string") data.displayName = displayName;
|
const allowedFields = ["displayName", "avatarUrl", "jobTitle", "company", "bio"];
|
||||||
if (typeof avatarUrl === "string") data.avatarUrl = avatarUrl;
|
for (const field of allowedFields) {
|
||||||
|
if (typeof body[field] === "string") data[field] = body[field];
|
||||||
|
}
|
||||||
|
|
||||||
await prisma.user.update({
|
const user = await prisma.user.update({
|
||||||
where: { id: session.user.id },
|
where: { id: session.user.id },
|
||||||
data,
|
data,
|
||||||
|
select: { id: true, displayName: true, avatarUrl: true, jobTitle: true, company: true, bio: true, email: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true, user });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ export type UserProfile = {
|
||||||
company: string;
|
company: string;
|
||||||
bio: string;
|
bio: string;
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
|
hasPassword: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_PROFILE: UserProfile = {
|
const DEFAULT_PROFILE: UserProfile = {
|
||||||
|
|
@ -22,10 +23,9 @@ const DEFAULT_PROFILE: UserProfile = {
|
||||||
company: "",
|
company: "",
|
||||||
bio: "",
|
bio: "",
|
||||||
avatarUrl: "",
|
avatarUrl: "",
|
||||||
|
hasPassword: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const STORAGE_KEY = "echo-ocr-user-profile";
|
|
||||||
|
|
||||||
type UserProfileContextValue = {
|
type UserProfileContextValue = {
|
||||||
profile: UserProfile;
|
profile: UserProfile;
|
||||||
updateProfile: (updates: Partial<UserProfile>) => Promise<void>;
|
updateProfile: (updates: Partial<UserProfile>) => Promise<void>;
|
||||||
|
|
@ -49,32 +49,37 @@ function getInitials(name: string): string {
|
||||||
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadLocalProfile(): Partial<UserProfile> {
|
|
||||||
if (typeof window === "undefined") return {};
|
|
||||||
try {
|
|
||||||
const raw = localStorage.getItem(STORAGE_KEY);
|
|
||||||
if (raw) return JSON.parse(raw);
|
|
||||||
} catch {}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveLocalProfile(profile: Partial<UserProfile>) {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(profile));
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UserProfileProvider({ children }: { children: React.ReactNode }) {
|
export function UserProfileProvider({ children }: { children: React.ReactNode }) {
|
||||||
const { data: session, status, update: updateSession } = useSession();
|
const { data: session, status, update: updateSession } = useSession();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [localOverrides, setLocalOverrides] = React.useState<Partial<UserProfile>>({});
|
const [dbProfile, setDbProfile] = React.useState<Partial<UserProfile>>({});
|
||||||
const [mounted, setMounted] = React.useState(false);
|
const [mounted, setMounted] = React.useState(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
setLocalOverrides(loadLocalProfile());
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (status === "authenticated") {
|
||||||
|
fetch("/api/auth/me")
|
||||||
|
.then((r) => (r.ok ? r.json() : null))
|
||||||
|
.then((data) => {
|
||||||
|
if (data) {
|
||||||
|
setDbProfile({
|
||||||
|
displayName: data.displayName || "",
|
||||||
|
email: data.email || "",
|
||||||
|
avatarUrl: data.avatarUrl || "",
|
||||||
|
jobTitle: data.jobTitle || "",
|
||||||
|
company: data.company || "",
|
||||||
|
bio: data.bio || "",
|
||||||
|
hasPassword: data.hasPassword ?? false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
const loading = status === "loading";
|
const loading = status === "loading";
|
||||||
const isAuthenticated = status === "authenticated";
|
const isAuthenticated = status === "authenticated";
|
||||||
|
|
||||||
|
|
@ -89,23 +94,19 @@ export function UserProfileProvider({ children }: { children: React.ReactNode })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
...localOverrides,
|
...dbProfile,
|
||||||
...(session?.user?.name ? { displayName: session.user.displayName || session.user.name } : {}),
|
|
||||||
...(session?.user?.email ? { email: session.user.email } : {}),
|
...(session?.user?.email ? { email: session.user.email } : {}),
|
||||||
...(session?.user?.avatarUrl ? { avatarUrl: session.user.avatarUrl } : {}),
|
|
||||||
};
|
};
|
||||||
}, [session, localOverrides]);
|
}, [session, dbProfile]);
|
||||||
|
|
||||||
const updateProfile = React.useCallback(async (updates: Partial<UserProfile>) => {
|
const updateProfile = React.useCallback(async (updates: Partial<UserProfile>) => {
|
||||||
setLocalOverrides((prev) => {
|
setDbProfile((prev) => ({ ...prev, ...updates }));
|
||||||
const next = { ...prev, ...updates };
|
|
||||||
saveLocalProfile(next);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
|
|
||||||
const apiPayload: Record<string, string> = {};
|
const apiPayload: Record<string, string> = {};
|
||||||
if (typeof updates.displayName === "string") apiPayload.displayName = updates.displayName;
|
const persistFields = ["displayName", "avatarUrl", "jobTitle", "company", "bio"] as const;
|
||||||
if (typeof updates.avatarUrl === "string") apiPayload.avatarUrl = updates.avatarUrl;
|
for (const field of persistFields) {
|
||||||
|
if (typeof updates[field] === "string") apiPayload[field] = updates[field];
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(apiPayload).length > 0) {
|
if (Object.keys(apiPayload).length > 0) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -157,7 +158,7 @@ export function useUserProfile() {
|
||||||
const ctx = React.useContext(UserProfileContext);
|
const ctx = React.useContext(UserProfileContext);
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
return {
|
return {
|
||||||
profile: DEFAULT_PROFILE,
|
profile: { ...DEFAULT_PROFILE },
|
||||||
updateProfile: async () => {},
|
updateProfile: async () => {},
|
||||||
initials: "",
|
initials: "",
|
||||||
userId: undefined,
|
userId: undefined,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue