login fixes

This commit is contained in:
Michael Freno
2025-12-22 10:55:08 -05:00
parent 281654081d
commit 1dd852795e
5 changed files with 204 additions and 97 deletions

View File

@@ -39,6 +39,7 @@ interface ContributionDay {
} }
export function RightBarContent() { export function RightBarContent() {
const { setLeftBarVisible } = useBars();
const [githubCommits, setGithubCommits] = createSignal<GitCommit[]>([]); const [githubCommits, setGithubCommits] = createSignal<GitCommit[]>([]);
const [giteaCommits, setGiteaCommits] = createSignal<GitCommit[]>([]); const [giteaCommits, setGiteaCommits] = createSignal<GitCommit[]>([]);
const [githubActivity, setGithubActivity] = createSignal<ContributionDay[]>( const [githubActivity, setGithubActivity] = createSignal<ContributionDay[]>(
@@ -47,6 +48,12 @@ export function RightBarContent() {
const [giteaActivity, setGiteaActivity] = createSignal<ContributionDay[]>([]); const [giteaActivity, setGiteaActivity] = createSignal<ContributionDay[]>([]);
const [loading, setLoading] = createSignal(true); const [loading, setLoading] = createSignal(true);
const handleLinkClick = () => {
if (typeof window !== "undefined" && window.innerWidth < 768) {
setLeftBarVisible(false);
}
};
onMount(() => { onMount(() => {
// Fetch all data client-side only to avoid hydration mismatch // Fetch all data client-side only to avoid hydration mismatch
const fetchData = async () => { const fetchData = async () => {
@@ -83,7 +90,9 @@ export function RightBarContent() {
<Typewriter keepAlive={false} class="z-50 px-4 md:pt-4"> <Typewriter keepAlive={false} class="z-50 px-4 md:pt-4">
<ul class="flex flex-col gap-4"> <ul class="flex flex-col gap-4">
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold"> <li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
<a href="/contact">Contact Me</a> <a href="/contact" onClick={handleLinkClick}>
Contact Me
</a>
</li> </li>
<li> <li>
<a <a
@@ -114,6 +123,7 @@ export function RightBarContent() {
<li> <li>
<a <a
href="/resume" href="/resume"
onClick={handleLinkClick}
class="hover:text-subtext0 flex items-center gap-3 transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-105" class="hover:text-subtext0 flex items-center gap-3 transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-105"
> >
<span class="shaker rounded-full p-2"> <span class="shaker rounded-full p-2">
@@ -177,6 +187,12 @@ export function LeftBar() {
const [isMounted, setIsMounted] = createSignal(false); const [isMounted, setIsMounted] = createSignal(false);
const [signOutLoading, setSignOutLoading] = createSignal(false); const [signOutLoading, setSignOutLoading] = createSignal(false);
const handleLinkClick = () => {
if (typeof window !== "undefined" && window.innerWidth < 768) {
setLeftBarVisible(false);
}
};
const handleSignOut = async () => { const handleSignOut = async () => {
setSignOutLoading(true); setSignOutLoading(true);
try { try {
@@ -337,7 +353,9 @@ export function LeftBar() {
<div class="flex h-full flex-col overflow-y-auto"> <div class="flex h-full flex-col overflow-y-auto">
<Typewriter speed={10} keepAlive={10000} class="z-50 pr-8 pl-4"> <Typewriter speed={10} keepAlive={10000} class="z-50 pr-8 pl-4">
<h3 class="hover:text-subtext0 w-fit pt-6 text-center text-3xl underline transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-105"> <h3 class="hover:text-subtext0 w-fit pt-6 text-center text-3xl underline transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-105">
<a href="/">{formatDomainName(env.VITE_DOMAIN)}</a> <a href="/" onClick={handleLinkClick}>
{formatDomainName(env.VITE_DOMAIN)}
</a>
</h3> </h3>
</Typewriter> </Typewriter>
@@ -368,6 +386,7 @@ export function LeftBar() {
{(post) => ( {(post) => (
<a <a
href={`/blog/${post.title}`} href={`/blog/${post.title}`}
onClick={handleLinkClick}
class="hover:text-subtext0 block transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-105 hover:font-bold" class="hover:text-subtext0 block transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-105 hover:font-bold"
> >
<Typewriter class="flex flex-col" keepAlive={false}> <Typewriter class="flex flex-col" keepAlive={false}>
@@ -402,17 +421,25 @@ export function LeftBar() {
<Typewriter keepAlive={false}> <Typewriter keepAlive={false}>
<ul class="flex flex-col gap-4 py-6"> <ul class="flex flex-col gap-4 py-6">
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold"> <li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
<a href="/">Home</a> <a href="/" onClick={handleLinkClick}>
Home
</a>
</li> </li>
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold"> <li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
<a href="/blog">Blog</a> <a href="/blog" onClick={handleLinkClick}>
Blog
</a>
</li> </li>
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold"> <li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
<Show <Show
when={isMounted() && userInfo()?.isAuthenticated} when={isMounted() && userInfo()?.isAuthenticated}
fallback={<a href="/login">Login</a>} fallback={
<a href="/login" onClick={handleLinkClick}>
Login
</a>
}
> >
<a href="/account"> <a href="/account" onClick={handleLinkClick}>
Account Account
<Show when={userInfo()?.email}> <Show when={userInfo()?.email}>
<span class="text-subtext0 text-sm font-normal"> <span class="text-subtext0 text-sm font-normal">

View File

@@ -52,6 +52,9 @@ export default function PostForm(props: PostFormProps) {
props.initialData?.body props.initialData?.body
); );
const [hasSaved, setHasSaved] = createSignal(props.mode === "edit"); const [hasSaved, setHasSaved] = createSignal(props.mode === "edit");
const [createdPostId, setCreatedPostId] = createSignal<number | undefined>(
props.postId
);
// Mark initial load as complete after data is loaded (for edit mode) // Mark initial load as complete after data is loaded (for edit mode)
createEffect(() => { createEffect(() => {
@@ -75,12 +78,13 @@ export default function PostForm(props: PostFormProps) {
)) as string; )) as string;
} }
if (props.mode === "edit") { if (props.mode === "edit" || createdPostId()) {
// Update existing post (either in edit mode or if already created)
await api.database.updatePost.mutate({ await api.database.updatePost.mutate({
id: props.postId!, id: createdPostId() || props.postId!,
title: titleVal.replaceAll(" ", "_"), title: titleVal.replaceAll(" ", "_"),
subtitle: subtitle() || "", subtitle: subtitle() || "",
body: body() || "", body: body() || "Hello, World!",
banner_photo: banner_photo:
bannerImageKey !== "" bannerImageKey !== ""
? bannerImageKey ? bannerImageKey
@@ -92,21 +96,20 @@ export default function PostForm(props: PostFormProps) {
author_id: props.userID author_id: props.userID
}); });
} else { } else {
// Create mode: only save once // Create mode: only save once (first autosave)
if (!hasSaved()) { const result = await api.database.createPost.mutate({
await api.database.createPost.mutate({
category: "blog", category: "blog",
title: titleVal.replaceAll(" ", "_"), title: titleVal.replaceAll(" ", "_"),
subtitle: subtitle() || null, subtitle: subtitle() || null,
body: body() || null, body: body() || "Hello, World!",
banner_photo: bannerImageKey !== "" ? bannerImageKey : null, banner_photo: bannerImageKey !== "" ? bannerImageKey : null,
published: published(), published: published(),
tags: tags().length > 0 ? tags() : null, tags: tags().length > 0 ? tags() : null,
author_id: props.userID author_id: props.userID
}); });
setCreatedPostId(result.data as number);
setHasSaved(true); setHasSaved(true);
} }
}
showAutoSaveTrigger(); showAutoSaveTrigger();
} catch (err) { } catch (err) {
@@ -216,12 +219,13 @@ export default function PostForm(props: PostFormProps) {
)) as string; )) as string;
} }
if (props.mode === "edit") { if (props.mode === "edit" || createdPostId()) {
// Update existing post (either in edit mode or if autosave created it)
await api.database.updatePost.mutate({ await api.database.updatePost.mutate({
id: props.postId!, id: createdPostId() || props.postId!,
title: title().replaceAll(" ", "_"), title: title().replaceAll(" ", "_"),
subtitle: subtitle() || null, subtitle: subtitle() || null,
body: body() || null, body: body() || "Hello, World!",
banner_photo: banner_photo:
bannerImageKey !== "" bannerImageKey !== ""
? bannerImageKey ? bannerImageKey
@@ -233,16 +237,18 @@ export default function PostForm(props: PostFormProps) {
author_id: props.userID author_id: props.userID
}); });
} else { } else {
await api.database.createPost.mutate({ // Create new post
const result = await api.database.createPost.mutate({
category: "blog", category: "blog",
title: title().replaceAll(" ", "_"), title: title().replaceAll(" ", "_"),
subtitle: subtitle() || null, subtitle: subtitle() || null,
body: body() || null, body: body() || "Hello, World!",
banner_photo: bannerImageKey !== "" ? bannerImageKey : null, banner_photo: bannerImageKey !== "" ? bannerImageKey : null,
published: published(), published: published(),
tags: tags().length > 0 ? tags() : null, tags: tags().length > 0 ? tags() : null,
author_id: props.userID author_id: props.userID
}); });
setCreatedPostId(result.data as number);
} }
navigate(`/blog/${encodeURIComponent(title().replaceAll(" ", "_"))}`); navigate(`/blog/${encodeURIComponent(title().replaceAll(" ", "_"))}`);

View File

@@ -1382,7 +1382,7 @@ export default function TextEditor(props: TextEditorProps) {
{/* Language Selector Dropdown */} {/* Language Selector Dropdown */}
<Show when={showLanguageSelector()}> <Show when={showLanguageSelector()}>
<div <div
class="language-selector bg-mantle text-text border-surface2 fixed z-[110] max-h-64 w-48 overflow-y-auto rounded border shadow-lg" class="language-selector bg-mantle text-text border-surface2 fixed z-110 max-h-64 w-48 overflow-y-auto rounded border shadow-lg"
style={{ style={{
top: `${languageSelectorPosition().top}px`, top: `${languageSelectorPosition().top}px`,
left: `${languageSelectorPosition().left}px` left: `${languageSelectorPosition().left}px`
@@ -1405,7 +1405,7 @@ export default function TextEditor(props: TextEditorProps) {
{/* Table Grid Selector */} {/* Table Grid Selector */}
<Show when={showTableMenu()}> <Show when={showTableMenu()}>
<div <div
class="table-menu fixed z-[110]" class="table-menu fixed z-110"
style={{ style={{
top: `${tableMenuPosition().top}px`, top: `${tableMenuPosition().top}px`,
left: `${tableMenuPosition().left}px` left: `${tableMenuPosition().left}px`
@@ -1418,7 +1418,7 @@ export default function TextEditor(props: TextEditorProps) {
{/* Mermaid Template Selector */} {/* Mermaid Template Selector */}
<Show when={showMermaidTemplates()}> <Show when={showMermaidTemplates()}>
<div <div
class="mermaid-menu bg-mantle text-text border-surface2 fixed z-[110] max-h-96 w-56 overflow-y-auto rounded border shadow-lg" class="mermaid-menu bg-mantle text-text border-surface2 fixed z-110 max-h-96 w-56 overflow-y-auto rounded border shadow-lg"
style={{ style={{
top: `${mermaidMenuPosition().top}px`, top: `${mermaidMenuPosition().top}px`,
left: `${mermaidMenuPosition().left}px` left: `${mermaidMenuPosition().left}px`
@@ -1897,11 +1897,11 @@ export default function TextEditor(props: TextEditorProps) {
{/* Keyboard Help Modal */} {/* Keyboard Help Modal */}
<Show when={showKeyboardHelp()}> <Show when={showKeyboardHelp()}>
<div <div
class="bg-opacity-50 fixed inset-0 z-[110] flex items-center justify-center bg-black" class="bg-opacity-50 fixed inset-0 z-110 flex items-center justify-center bg-black"
onClick={() => setShowKeyboardHelp(false)} onClick={() => setShowKeyboardHelp(false)}
> >
<div <div
class="bg-base border-surface2 max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-lg border p-6 shadow-2xl" class="bg-base border-surface2 max-h-[80dvh] w-full max-w-2xl overflow-y-auto rounded-lg border p-6 shadow-2xl"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{/* Header */} {/* Header */}

View File

@@ -5,6 +5,7 @@ import CountdownCircleTimer from "~/components/CountdownCircleTimer";
import Eye from "~/components/icons/Eye"; import Eye from "~/components/icons/Eye";
import EyeSlash from "~/components/icons/EyeSlash"; import EyeSlash from "~/components/icons/EyeSlash";
import { validatePassword } from "~/lib/validation"; import { validatePassword } from "~/lib/validation";
import { api } from "~/lib/api";
export default function PasswordResetPage() { export default function PasswordResetPage() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -67,32 +68,26 @@ export default function PasswordResetPage() {
setPasswordChangeLoading(true); setPasswordChangeLoading(true);
try { try {
const response = await fetch("/api/trpc/auth.resetPassword", { const result = await api.auth.resetPassword.mutate({
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
token: token, token: token,
newPassword, newPassword,
newPasswordConfirmation: newPasswordConf newPasswordConfirmation: newPasswordConf
})
}); });
const result = await response.json(); if (result.success) {
if (response.ok && result.result?.data) {
setCountDown(true); setCountDown(true);
} else { } else {
const errorMsg = result.error?.message || "Failed to reset password"; setError("Failed to reset password");
}
} catch (err: any) {
console.error("Password reset error:", err);
const errorMsg = err.message || "An error occurred. Please try again.";
if (errorMsg.includes("expired") || errorMsg.includes("token")) { if (errorMsg.includes("expired") || errorMsg.includes("token")) {
setShowRequestNewEmail(true); setShowRequestNewEmail(true);
setError("Token has expired"); setError("Token has expired");
} else { } else {
setError(errorMsg); setError(errorMsg);
} }
}
} catch (err) {
console.error("Password reset error:", err);
setError("An error occurred. Please try again.");
} finally { } finally {
setPasswordChangeLoading(false); setPasswordChangeLoading(false);
} }
@@ -192,27 +187,48 @@ export default function PasswordResetPage() {
> >
<div class="flex w-full max-w-md flex-col justify-center px-4"> <div class="flex w-full max-w-md flex-col justify-center px-4">
{/* New Password Input */} {/* New Password Input */}
<div class="input-group relative mx-4"> <div class="flex justify-center">
<div class="input-group mx-4 flex">
<input <input
ref={newPasswordRef} ref={newPasswordRef}
name="newPassword" name="newPassword"
type={showPasswordInput() ? "text" : "password"} type={showPasswordInput() ? "text" : "password"}
required required
autofocus
onInput={handleNewPasswordChange} onInput={handleNewPasswordChange}
onBlur={handlePasswordBlur} onBlur={handlePasswordBlur}
disabled={passwordChangeLoading()} disabled={passwordChangeLoading()}
placeholder=" " placeholder=" "
class="underlinedInput w-full bg-transparent pr-10" class="underlinedInput bg-transparent"
/> />
<span class="bar"></span> <span class="bar"></span>
<label class="underlinedInputLabel">New Password</label> <label class="underlinedInputLabel">New Password</label>
</div>
<button <button
type="button" type="button"
onClick={() => setShowPasswordInput(!showPasswordInput())} onClick={() => {
class="absolute top-2 right-0 text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200" setShowPasswordInput(!showPasswordInput());
newPasswordRef?.focus();
}}
class="absolute mt-14 ml-60"
> >
<Show when={showPasswordInput()} fallback={<Eye />}> <Show
<EyeSlash /> when={showPasswordInput()}
fallback={
<EyeSlash
height={24}
width={24}
strokeWidth={1}
class="stroke-zinc-900 dark:stroke-white"
/>
}
>
<Eye
height={24}
width={24}
strokeWidth={1}
class="stroke-zinc-900 dark:stroke-white"
/>
</Show> </Show>
</button> </button>
</div> </div>
@@ -221,13 +237,14 @@ export default function PasswordResetPage() {
<div <div
class={`${ class={`${
showPasswordLengthWarning() ? "" : "opacity-0 select-none" showPasswordLengthWarning() ? "" : "opacity-0 select-none"
} mt-2 text-center text-sm text-red-500 transition-opacity duration-200 ease-in-out`} } text-center text-red-500 transition-opacity duration-200 ease-in-out`}
> >
Password too short! Min Length: 8 Password too short! Min Length: 8
</div> </div>
{/* Password Confirmation Input */} {/* Password Confirmation Input */}
<div class="input-group relative mx-4 mt-6"> <div class="-mt-4 flex justify-center">
<div class="input-group mx-4 flex">
<input <input
ref={newPasswordConfRef} ref={newPasswordConfRef}
name="newPasswordConf" name="newPasswordConf"
@@ -236,19 +253,38 @@ export default function PasswordResetPage() {
required required
disabled={passwordChangeLoading()} disabled={passwordChangeLoading()}
placeholder=" " placeholder=" "
class="underlinedInput w-full bg-transparent pr-10" class="underlinedInput bg-transparent"
/> />
<span class="bar"></span> <span class="bar"></span>
<label class="underlinedInputLabel">Password Confirmation</label> <label class="underlinedInputLabel">
Password Confirmation
</label>
</div>
<button <button
type="button" type="button"
onClick={() => onClick={() => {
setShowPasswordConfInput(!showPasswordConfInput()) setShowPasswordConfInput(!showPasswordConfInput());
} newPasswordConfRef?.focus();
class="absolute top-2 right-0 text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200" }}
class="absolute mt-14 ml-60"
> >
<Show when={showPasswordConfInput()} fallback={<Eye />}> <Show
<EyeSlash /> when={showPasswordConfInput()}
fallback={
<EyeSlash
height={24}
width={24}
strokeWidth={1}
class="stroke-zinc-900 dark:stroke-white"
/>
}
>
<Eye
height={24}
width={24}
strokeWidth={1}
class="stroke-zinc-900 dark:stroke-white"
/>
</Show> </Show>
</button> </button>
</div> </div>
@@ -262,7 +298,7 @@ export default function PasswordResetPage() {
newPasswordConfRef.value.length >= 6 newPasswordConfRef.value.length >= 6
? "" ? ""
: "opacity-0 select-none" : "opacity-0 select-none"
} mt-2 text-center text-sm text-red-500 transition-opacity duration-200 ease-in-out`} } text-center text-red-500 transition-opacity duration-200 ease-in-out`}
> >
Passwords do not match! Passwords do not match!
</div> </div>

View File

@@ -600,8 +600,8 @@ export const authRouter = createTRPCRouter({
const conn = ConnectionFactory(); const conn = ConnectionFactory();
const res = await conn.execute({ const res = await conn.execute({
sql: "SELECT * FROM User WHERE email = ? AND provider = ?", sql: "SELECT * FROM User WHERE email = ?",
args: [email, "email"] args: [email]
}); });
if (res.rows.length === 0) { if (res.rows.length === 0) {
@@ -629,6 +629,17 @@ export const authRouter = createTRPCRouter({
}); });
} }
// If provider is unknown/null, update it to "email" since they're logging in with password
if (
!user.provider ||
!["email", "google", "github", "apple"].includes(user.provider)
) {
await conn.execute({
sql: "UPDATE User SET provider = ? WHERE id = ?",
args: ["email", user.id]
});
}
// Create JWT token with appropriate expiry // Create JWT token with appropriate expiry
const expiresIn = rememberMe ? "14d" : "12h"; const expiresIn = rememberMe ? "14d" : "12h";
const token = await createJWT(user.id, expiresIn); const token = await createJWT(user.id, expiresIn);
@@ -940,10 +951,37 @@ export const authRouter = createTRPCRouter({
const conn = ConnectionFactory(); const conn = ConnectionFactory();
const passwordHash = await hashPassword(newPassword); const passwordHash = await hashPassword(newPassword);
// Get user to check current provider
const userRes = await conn.execute({
sql: "SELECT provider FROM User WHERE id = ?",
args: [payload.id]
});
if (userRes.rows.length === 0) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found"
});
}
const currentProvider = (userRes.rows[0] as any).provider;
// Only update provider to "email" if it's null, undefined, or not a known OAuth provider
if (
!currentProvider ||
!["google", "github", "apple"].includes(currentProvider)
) {
await conn.execute({
sql: "UPDATE User SET password_hash = ?, provider = ? WHERE id = ?",
args: [passwordHash, "email", payload.id]
});
} else {
// Keep existing OAuth provider, just update password
await conn.execute({ await conn.execute({
sql: "UPDATE User SET password_hash = ? WHERE id = ?", sql: "UPDATE User SET password_hash = ? WHERE id = ?",
args: [passwordHash, payload.id] args: [passwordHash, payload.id]
}); });
}
// Clear any session cookies // Clear any session cookies
setCookie(ctx.event.nativeEvent, "emailToken", "", { setCookie(ctx.event.nativeEvent, "emailToken", "", {