adjust email/pass addition flow, blog ttl extended

This commit is contained in:
Michael Freno
2026-01-07 21:21:27 -05:00
parent ca28237d13
commit 8f241ce611
6 changed files with 92 additions and 23 deletions

View File

@@ -64,7 +64,6 @@ const WARMUP_RUNS = 1;
// Pages to test // Pages to test
const TEST_PAGES: PageTestConfig[] = [ const TEST_PAGES: PageTestConfig[] = [
{ name: "Home", path: "/" }, { name: "Home", path: "/" },
{ name: "About", path: "/about" },
{ name: "Blog Index", path: "/blog" }, { name: "Blog Index", path: "/blog" },
{ name: "Blog Post (basic)", path: "/blog/I_made_a_macOS_app_in_a_day" }, { name: "Blog Post (basic)", path: "/blog/I_made_a_macOS_app_in_a_day" },
{ {

View File

@@ -147,7 +147,7 @@ export const COOLDOWN_TIMERS = {
export const CACHE_CONFIG = { export const CACHE_CONFIG = {
BLOG_CACHE_TTL_MS: 24 * 60 * 60 * 1000, BLOG_CACHE_TTL_MS: 24 * 60 * 60 * 1000,
GIT_ACTIVITY_CACHE_TTL_MS: 10 * 60 * 1000, GIT_ACTIVITY_CACHE_TTL_MS: 10 * 60 * 1000,
BLOG_POSTS_LIST_CACHE_TTL_MS: 5 * 60 * 1000, BLOG_POSTS_LIST_CACHE_TTL_MS: 15 * 60 * 1000,
MAX_STALE_DATA_MS: 7 * 24 * 60 * 60 * 1000, MAX_STALE_DATA_MS: 7 * 24 * 60 * 60 * 1000,
GIT_ACTIVITY_MAX_STALE_MS: 24 * 60 * 60 * 1000 GIT_ACTIVITY_MAX_STALE_MS: 24 * 60 * 60 * 1000
} as const; } as const;

View File

@@ -155,6 +155,8 @@ export const model: { [key: string]: string } = {
post_id INTEGER NOT NULL post_id INTEGER NOT NULL
); );
CREATE INDEX IF NOT EXISTS idx_tag_post_id ON Tag (post_id); CREATE INDEX IF NOT EXISTS idx_tag_post_id ON Tag (post_id);
CREATE INDEX IF NOT EXISTS idx_tag_value ON Tag (value);
CREATE INDEX IF NOT EXISTS idx_tag_post_value ON Tag (post_id, value);
`, `,
PostHistory: ` PostHistory: `
CREATE TABLE PostHistory CREATE TABLE PostHistory

View File

@@ -16,6 +16,7 @@ import PasswordInput from "~/components/ui/PasswordInput";
import Button from "~/components/ui/Button"; import Button from "~/components/ui/Button";
import FormFeedback from "~/components/ui/FormFeedback"; import FormFeedback from "~/components/ui/FormFeedback";
import type { UserProfile } from "~/types/user"; import type { UserProfile } from "~/types/user";
import { useAuth } from "~/context/auth";
const getUserProfile = query(async (): Promise<UserProfile | null> => { const getUserProfile = query(async (): Promise<UserProfile | null> => {
"use server"; "use server";
@@ -27,13 +28,11 @@ const getUserProfile = query(async (): Promise<UserProfile | null> => {
throw redirect("/login"); throw redirect("/login");
} }
const userId = userState.userId;
const conn = ConnectionFactory(); const conn = ConnectionFactory();
try { try {
const res = await conn.execute({ const res = await conn.execute({
sql: "SELECT * FROM User WHERE id = ?", sql: "SELECT provider, image, password_hash FROM User WHERE id = ?",
args: [userId] args: [userState.userId]
}); });
if (res.rows.length === 0) { if (res.rows.length === 0) {
@@ -43,10 +42,10 @@ const getUserProfile = query(async (): Promise<UserProfile | null> => {
const user = res.rows[0] as any; const user = res.rows[0] as any;
return { return {
id: user.id, id: userState.userId,
email: user.email ?? undefined, email: userState.email ?? undefined,
emailVerified: user.email_verified === 1, emailVerified: userState.emailVerified,
displayName: user.display_name ?? undefined, displayName: userState.displayName ?? undefined,
provider: user.provider ?? undefined, provider: user.provider ?? undefined,
image: user.image ?? undefined, image: user.image ?? undefined,
hasPassword: !!user.password_hash hasPassword: !!user.password_hash
@@ -63,6 +62,7 @@ export const route = {
export default function AccountPage() { export default function AccountPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const { refreshAuth } = useAuth();
const userData = createAsync(() => getUserProfile(), { deferStream: true }); const userData = createAsync(() => getUserProfile(), { deferStream: true });
@@ -451,6 +451,7 @@ export default function AccountPage() {
setSignOutLoading(true); setSignOutLoading(true);
try { try {
await api.auth.signOut.mutate(); await api.auth.signOut.mutate();
refreshAuth();
navigate("/"); navigate("/");
} catch (error) { } catch (error) {
console.error("Sign out failed:", error); console.error("Sign out failed:", error);
@@ -555,7 +556,11 @@ export default function AccountPage() {
} }
> >
<div class="bg-blue mt-3 rounded px-3 py-2 text-center text-base text-sm"> <div class="bg-blue mt-3 rounded px-3 py-2 text-center text-base text-sm">
💡 Add a password to enable email/password login {!userProfile().email
? "💡 Add and verify an email to enable email/password login"
: !userProfile().emailVerified
? "💡 Verify your email to enable password setup"
: "💡 Add a password to enable email/password login"}
</div> </div>
</Show> </Show>
</div> </div>
@@ -769,6 +774,42 @@ export default function AccountPage() {
</div> </div>
</noscript> </noscript>
<Show when={!userProfile().hasPassword}> <Show when={!userProfile().hasPassword}>
<Show
when={
userProfile().provider !== "email" &&
(!userProfile().email || !userProfile().emailVerified)
}
>
<div class="bg-yellow mb-4 rounded px-4 py-3 text-center text-base text-sm">
<div class="mb-1 font-semibold">
Email Verification Required
</div>
<div>
{!userProfile().email
? "Please add and verify an email address before setting a password."
: "Please verify your email address before setting a password."}
</div>
<Show
when={
userProfile().email &&
!userProfile().emailVerified
}
>
<button
onClick={sendEmailVerification}
class="mt-2 font-semibold text-blue-700 underline transition-all hover:brightness-125"
>
Resend Verification Email
</button>
</Show>
</div>
</Show>
<Show
when={
userProfile().provider === "email" ||
(userProfile().email && userProfile().emailVerified)
}
>
<div class="text-subtext0 mb-4 text-center text-sm"> <div class="text-subtext0 mb-4 text-center text-sm">
{userProfile().provider === "email" {userProfile().provider === "email"
? "Set a password to enable password login" ? "Set a password to enable password login"
@@ -777,6 +818,7 @@ export default function AccountPage() {
" login"} " login"}
</div> </div>
</Show> </Show>
</Show>
<Show when={userProfile().hasPassword}> <Show when={userProfile().hasPassword}>
<PasswordInput <PasswordInput
@@ -795,7 +837,13 @@ export default function AccountPage() {
minlength="8" minlength="8"
onInput={handleNewPasswordChange} onInput={handleNewPasswordChange}
onBlur={handlePasswordBlur} onBlur={handlePasswordBlur}
disabled={passwordChangeLoading()} disabled={
passwordChangeLoading() ||
(!userProfile().hasPassword &&
userProfile().provider !== "email" &&
(!userProfile().email ||
!userProfile().emailVerified))
}
title="Password must be at least 8 characters" title="Password must be at least 8 characters"
label="New Password" label="New Password"
showStrength showStrength
@@ -806,7 +854,13 @@ export default function AccountPage() {
required required
minlength="8" minlength="8"
onInput={handlePasswordConfChange} onInput={handlePasswordConfChange}
disabled={passwordChangeLoading()} disabled={
passwordChangeLoading() ||
(!userProfile().hasPassword &&
userProfile().provider !== "email" &&
(!userProfile().email ||
!userProfile().emailVerified))
}
title="Password must be at least 8 characters" title="Password must be at least 8 characters"
label="New Password Conf." label="New Password Conf."
/> />
@@ -828,7 +882,13 @@ export default function AccountPage() {
<Button <Button
type="submit" type="submit"
disabled={!passwordsMatch()} disabled={
!passwordsMatch() ||
(!userProfile().hasPassword &&
userProfile().provider !== "email" &&
(!userProfile().email ||
!userProfile().emailVerified))
}
loading={passwordChangeLoading()} loading={passwordChangeLoading()}
class="my-6" class="my-6"
> >

View File

@@ -13,11 +13,11 @@ const getPosts = query(async () => {
"use server"; "use server";
const { getUserState } = await import("~/lib/auth-query"); const { getUserState } = await import("~/lib/auth-query");
const { ConnectionFactory } = await import("~/server/utils"); const { ConnectionFactory } = await import("~/server/utils");
const { withCache } = await import("~/server/cache"); const { withCacheAndStale } = await import("~/server/cache");
const userState = await getUserState(); const userState = await getUserState();
const privilegeLevel = userState.privilegeLevel; const privilegeLevel = userState.privilegeLevel;
return withCache( return withCacheAndStale(
`posts-${privilegeLevel}`, `posts-${privilegeLevel}`,
CACHE_CONFIG.BLOG_POSTS_LIST_CACHE_TTL_MS, CACHE_CONFIG.BLOG_POSTS_LIST_CACHE_TTL_MS,
async () => { async () => {

View File

@@ -244,6 +244,14 @@ export const userRouter = createTRPCRouter({
}); });
} }
// For OAuth accounts, require verified email before setting password
if (user.provider !== "email" && (!user.email || !user.email_verified)) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Email verification required before setting password"
});
}
const passwordHash = await hashPassword(newPassword); const passwordHash = await hashPassword(newPassword);
await conn.execute({ await conn.execute({
sql: "UPDATE User SET password_hash = ? WHERE id = ?", sql: "UPDATE User SET password_hash = ? WHERE id = ?",