From 11fad35288f777e7ad4eecfc7e0a3fd9a02339ea Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Mon, 22 Dec 2025 16:44:43 -0500 Subject: [PATCH] good enough --- src/components/Bars.tsx | 8 +- src/components/blog/Card.tsx | 2 +- src/routes/account.tsx | 232 +++++++++++++------- src/routes/api/auth/signout.ts | 25 +++ src/routes/blog/[title]/index.tsx | 156 ++++++------- src/routes/contact.tsx | 3 + src/routes/login/index.tsx | 41 ++-- src/routes/login/request-password-reset.tsx | 1 + src/server/conditional-parser.ts | 72 +++--- 9 files changed, 332 insertions(+), 208 deletions(-) create mode 100644 src/routes/api/auth/signout.ts diff --git a/src/components/Bars.tsx b/src/components/Bars.tsx index a6f3077..980ad5c 100644 --- a/src/components/Bars.tsx +++ b/src/components/Bars.tsx @@ -309,7 +309,10 @@ export function LeftBar() { return ( + ); } diff --git a/src/components/blog/Card.tsx b/src/components/blog/Card.tsx index 7f5e3e4..6800cc7 100644 --- a/src/components/blog/Card.tsx +++ b/src/components/blog/Card.tsx @@ -39,7 +39,7 @@ export default function Card(props: CardProps) { {props.post.title.replaceAll("_", diff --git a/src/routes/account.tsx b/src/routes/account.tsx index 04a4ce0..697fcda 100644 --- a/src/routes/account.tsx +++ b/src/routes/account.tsx @@ -1,6 +1,6 @@ -import { createSignal, Show, onMount } from "solid-js"; +import { createSignal, Show, createEffect } from "solid-js"; import { Title, Meta } from "@solidjs/meta"; -import { useNavigate, redirect, query } from "@solidjs/router"; +import { useNavigate, redirect, query, createAsync } from "@solidjs/router"; import { getEvent } from "vinxi/http"; import Eye from "~/components/icons/Eye"; import EyeSlash from "~/components/icons/EyeSlash"; @@ -23,29 +23,56 @@ type UserProfile = { hasPassword: boolean; }; -const checkAuth = query(async () => { +const getUserProfile = query(async (): Promise => { "use server"; - const { checkAuthStatus } = await import("~/server/utils"); + const { getUserID, ConnectionFactory } = await import("~/server/utils"); const event = getEvent()!; - const { isAuthenticated } = await checkAuthStatus(event); - if (!isAuthenticated) { + const userId = await getUserID(event); + if (!userId) { throw redirect("/login"); } - return { isAuthenticated }; -}, "accountAuthCheck"); + const conn = ConnectionFactory(); + try { + const res = await conn.execute({ + sql: "SELECT * FROM User WHERE id = ?", + args: [userId] + }); + + if (res.rows.length === 0) { + throw redirect("/login"); + } + + const user = res.rows[0] as any; + + // Transform to UserProfile type + return { + id: user.id, + email: user.email || null, + emailVerified: Boolean(user.emailVerified), + displayName: user.displayName || null, + image: user.image || null, + provider: user.provider || null, + hasPassword: Boolean(user.password) + }; + } catch (err) { + console.error("Failed to fetch user profile:", err); + throw redirect("/login"); + } +}, "accountUserProfile"); export const route = { - load: () => checkAuth() + load: () => getUserProfile() }; export default function AccountPage() { const navigate = useNavigate(); - // User data + const userData = createAsync(() => getUserProfile(), { deferStream: true }); + + // Local user state for client-side updates const [user, setUser] = createSignal(null); - const [loading, setLoading] = createSignal(true); // Form loading states const [emailButtonLoading, setEmailButtonLoading] = createSignal(false); @@ -98,27 +125,14 @@ export default function AccountPage() { let displayNameRef: HTMLInputElement | undefined; let deleteAccountPasswordRef: HTMLInputElement | undefined; - // Fetch user profile on mount - onMount(async () => { - try { - const response = await fetch("/api/trpc/user.getProfile", { - method: "GET" - }); + // Helper to get current user (from SSR data or local state) + const currentUser = () => user() || userData(); - if (response.ok) { - const result = await response.json(); - if (result.result?.data) { - setUser(result.result.data); - // Set preset holder if user has existing image - if (result.result.data.image) { - setPreSetHolder(result.result.data.image); - } - } - } - } catch (err) { - console.error("Failed to fetch user profile:", err); - } finally { - setLoading(false); + // Initialize preSetHolder when userData loads + createEffect(() => { + const userProfile = userData(); + if (userProfile?.image && !preSetHolder()) { + setPreSetHolder(userProfile.image); } }); @@ -152,8 +166,8 @@ export default function AccountPage() { setProfileImageSetLoading(true); setShowImageSuccess(false); - const currentUser = user(); - if (!currentUser) { + const userProfile = currentUser(); + if (!userProfile) { setProfileImageSetLoading(false); return; } @@ -161,17 +175,15 @@ export default function AccountPage() { try { let imageUrl = ""; - // Upload new image if one was selected if (profileImage()) { const imageKey = await AddImageToS3( profileImage()!, - currentUser.id, + userProfile.id, "user" ); imageUrl = imageKey || ""; } - // Update user profile image const response = await fetch("/api/trpc/user.updateProfileImage", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -264,10 +276,10 @@ export default function AccountPage() { // Password change/set handler const handlePasswordSubmit = async (e: Event) => { e.preventDefault(); - const currentUser = user(); - if (!currentUser) return; + const userProfile = currentUser(); + if (!userProfile) return; - if (currentUser.hasPassword) { + if (userProfile.hasPassword) { // Change password (requires old password) if (!oldPasswordRef || !newPasswordRef || !newPasswordConfRef) return; @@ -399,14 +411,14 @@ export default function AccountPage() { // Resend email verification const sendEmailVerification = async () => { - const currentUser = user(); - if (!currentUser?.email) return; + const userProfile = currentUser(); + if (!userProfile?.email) return; try { await fetch("/api/trpc/auth.resendEmailVerification", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email: currentUser.email }) + body: JSON.stringify({ email: userProfile.email }) }); alert("Verification email sent!"); } catch (err) { @@ -496,9 +508,20 @@ export default function AccountPage() { />
+ +
- }> - {(currentUser) => ( + }> + {(userProfile) => ( <>
Account Settings @@ -512,18 +535,18 @@ export default function AccountPage() {
- {getProviderInfo(currentUser().provider).icon} + {getProviderInfo(userProfile().provider).icon} - {getProviderInfo(currentUser().provider).name} Account + {getProviderInfo(userProfile().provider).name} Account
@@ -532,8 +555,8 @@ export default function AccountPage() {
@@ -551,12 +574,17 @@ export default function AccountPage() {
Profile Image
-
+ +
+ +
+ +
+ {/* Delete Account Section */}
@@ -903,12 +969,18 @@ export default function AccountPage() { irreversible
+ +
- Your {getProviderInfo(currentUser().provider).name}{" "} + Your {getProviderInfo(userProfile().provider).name}{" "} account doesn't have a password. To delete your account, please set a password first, then return here to proceed with deletion. @@ -931,8 +1003,10 @@ export default function AccountPage() { ref={deleteAccountPasswordRef} type="password" required + minlength="8" disabled={deleteAccountButtonLoading()} placeholder=" " + title="Enter your password to confirm account deletion" class="underlinedInput bg-transparent" /> diff --git a/src/routes/api/auth/signout.ts b/src/routes/api/auth/signout.ts new file mode 100644 index 0000000..113ea16 --- /dev/null +++ b/src/routes/api/auth/signout.ts @@ -0,0 +1,25 @@ +import type { APIEvent } from "@solidjs/start/server"; +import { getCookie, getEvent, setCookie } from "vinxi/http"; + +export async function POST() { + "use server"; + const event = getEvent()!; + + // Clear the userIDToken cookie (the actual session cookie) + setCookie(event, "userIDToken", "", { + path: "/", + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 0, // Expire immediately + expires: new Date(0) // Set expiry to past date + }); + + // Redirect to home page + return new Response(null, { + status: 302, + headers: { + Location: "/" + } + }); +} diff --git a/src/routes/blog/[title]/index.tsx b/src/routes/blog/[title]/index.tsx index 7ffee8e..2273184 100644 --- a/src/routes/blog/[title]/index.tsx +++ b/src/routes/blog/[title]/index.tsx @@ -312,93 +312,93 @@ export default function PostPage() {
{/* Spacer to push content down */} -
- - {/* Content that slides over the fixed image */} -
-
-
-
-
- Written {new Date(p().date).toDateString()} -
- By Michael Freno +
+ {/* Content that slides over the fixed image */} +
+
+
+
+
+ Written {new Date(p().date).toDateString()} +
+ By Michael Freno +
+
+
+ + {(tag) => ( +
+
{tag.value}
+
+ )} +
-
diff --git a/src/routes/contact.tsx b/src/routes/contact.tsx index 23f5545..e345772 100644 --- a/src/routes/contact.tsx +++ b/src/routes/contact.tsx @@ -215,6 +215,7 @@ export default function ContactPage() { name="name" value={user()?.displayName ?? ""} placeholder=" " + title="Please enter your name" class="underlinedInput w-full bg-transparent" /> @@ -227,6 +228,7 @@ export default function ContactPage() { name="email" value={user()?.email ?? ""} placeholder=" " + title="Please enter a valid email address" class="underlinedInput w-full bg-transparent" /> @@ -239,6 +241,7 @@ export default function ContactPage() { required name="message" placeholder=" " + title="Please enter your message" class="underlinedInput w-full bg-transparent" rows={4} /> diff --git a/src/routes/login/index.tsx b/src/routes/login/index.tsx index 80af48a..cb49224 100644 --- a/src/routes/login/index.tsx +++ b/src/routes/login/index.tsx @@ -38,11 +38,13 @@ export default function LoginPage() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); + // Derive state directly from URL parameters (no signals needed) + const register = () => searchParams.mode === "register"; + const usePassword = () => searchParams.auth === "password"; + // State management - const [register, setRegister] = createSignal(false); const [error, setError] = createSignal(""); const [loading, setLoading] = createSignal(false); - const [usePassword, setUsePassword] = createSignal(false); const [countDown, setCountDown] = createSignal(0); const [emailSent, setEmailSent] = createSignal(false); const [showPasswordError, setShowPasswordError] = createSignal(false); @@ -370,29 +372,23 @@ export default function LoginPage() { fallback={
Already have an account? - +
} >
Don't have an account yet? - +
@@ -406,6 +402,7 @@ export default function LoginPage() { required ref={emailRef} placeholder=" " + title="Please enter a valid email address" class="underlinedInput bg-transparent" /> @@ -425,6 +422,7 @@ export default function LoginPage() { onInput={register() ? handleNewPasswordChange : undefined} onBlur={register() ? handlePasswordBlur : undefined} placeholder=" " + title="Password must be at least 8 characters" class="underlinedInput bg-transparent" /> @@ -478,6 +476,7 @@ export default function LoginPage() { ref={passwordConfRef} onInput={handlePasswordConfChange} placeholder=" " + title="Password must be at least 8 characters and match the password above" class="underlinedInput bg-transparent" /> @@ -584,22 +583,20 @@ export default function LoginPage() { {/* Toggle password/email link */} - + - +
diff --git a/src/routes/login/request-password-reset.tsx b/src/routes/login/request-password-reset.tsx index 612f6a0..571fce5 100644 --- a/src/routes/login/request-password-reset.tsx +++ b/src/routes/login/request-password-reset.tsx @@ -144,6 +144,7 @@ export default function RequestPasswordResetPage() { required disabled={loading()} placeholder=" " + title="Please enter a valid email address" class="underlinedInput w-full bg-transparent" /> diff --git a/src/server/conditional-parser.ts b/src/server/conditional-parser.ts index 110cf01..3a4d29a 100644 --- a/src/server/conditional-parser.ts +++ b/src/server/conditional-parser.ts @@ -63,27 +63,37 @@ function processBlockConditionals( html: string, context: ConditionalContext ): string { - // Regex to match conditional blocks - // Matches:
...
- const conditionalRegex = - /]*class="[^"]*conditional-block[^"]*"[^>]*data-condition-type="([^"]+)"[^>]*data-condition-value="([^"]+)"[^>]*data-show-when="(true|false)"[^>]*>([\s\S]*?)<\/div>/gi; + // More flexible regex that handles attributes in any order + // Match div with class="conditional-block" and capture the full tag + const divRegex = + /]*class="[^"]*conditional-block[^"]*"[^>]*)>([\s\S]*?)<\/div>/gi; let processedHtml = html; let match: RegExpExecArray | null; // Reset regex lastIndex - conditionalRegex.lastIndex = 0; + divRegex.lastIndex = 0; // Collect all matches first to avoid regex state issues const matches: ConditionalBlock[] = []; - while ((match = conditionalRegex.exec(html)) !== null) { - matches.push({ - fullMatch: match[0], - conditionType: match[1], - conditionValue: match[2], - showWhen: match[3], - content: match[4] - }); + while ((match = divRegex.exec(html)) !== null) { + const attributes = match[1]; + const content = match[2]; + + // Extract individual attributes + const typeMatch = /data-condition-type="([^"]+)"/.exec(attributes); + const valueMatch = /data-condition-value="([^"]+)"/.exec(attributes); + const showWhenMatch = /data-show-when="(true|false)"/.exec(attributes); + + if (typeMatch && valueMatch && showWhenMatch) { + matches.push({ + fullMatch: match[0], + conditionType: typeMatch[1], + conditionValue: valueMatch[1], + showWhen: showWhenMatch[1], + content: content + }); + } } // Process each conditional block @@ -120,27 +130,37 @@ function processInlineConditionals( html: string, context: ConditionalContext ): string { - // Regex to match inline conditionals - // Matches: ... - const inlineRegex = - /]*class="[^"]*conditional-inline[^"]*"[^>]*data-condition-type="([^"]+)"[^>]*data-condition-value="([^"]+)"[^>]*data-show-when="(true|false)"[^>]*>([\s\S]*?)<\/span>/gi; + // More flexible regex that handles attributes in any order + // Match span with class="conditional-inline" and capture the full tag + const spanRegex = + /]*class="[^"]*conditional-inline[^"]*"[^>]*)>([\s\S]*?)<\/span>/gi; let processedHtml = html; let match: RegExpExecArray | null; // Reset regex lastIndex - inlineRegex.lastIndex = 0; + spanRegex.lastIndex = 0; // Collect all matches first const matches: ConditionalBlock[] = []; - while ((match = inlineRegex.exec(html)) !== null) { - matches.push({ - fullMatch: match[0], - conditionType: match[1], - conditionValue: match[2], - showWhen: match[3], - content: match[4] - }); + while ((match = spanRegex.exec(html)) !== null) { + const attributes = match[1]; + const content = match[2]; + + // Extract individual attributes + const typeMatch = /data-condition-type="([^"]+)"/.exec(attributes); + const valueMatch = /data-condition-value="([^"]+)"/.exec(attributes); + const showWhenMatch = /data-show-when="(true|false)"/.exec(attributes); + + if (typeMatch && valueMatch && showWhenMatch) { + matches.push({ + fullMatch: match[0], + conditionType: typeMatch[1], + conditionValue: valueMatch[1], + showWhen: showWhenMatch[1], + content: content + }); + } } // Process each inline conditional