adjust email/pass addition flow, blog ttl extended
This commit is contained in:
@@ -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" },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,13 +774,50 @@ export default function AccountPage() {
|
|||||||
</div>
|
</div>
|
||||||
</noscript>
|
</noscript>
|
||||||
<Show when={!userProfile().hasPassword}>
|
<Show when={!userProfile().hasPassword}>
|
||||||
<div class="text-subtext0 mb-4 text-center text-sm">
|
<Show
|
||||||
{userProfile().provider === "email"
|
when={
|
||||||
? "Set a password to enable password login"
|
userProfile().provider !== "email" &&
|
||||||
: "Add a password to enable email/password login alongside your " +
|
(!userProfile().email || !userProfile().emailVerified)
|
||||||
getProviderName(userProfile().provider) +
|
}
|
||||||
" login"}
|
>
|
||||||
</div>
|
<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">
|
||||||
|
{userProfile().provider === "email"
|
||||||
|
? "Set a password to enable password login"
|
||||||
|
: "Add a password to enable email/password login alongside your " +
|
||||||
|
getProviderName(userProfile().provider) +
|
||||||
|
" login"}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={userProfile().hasPassword}>
|
<Show when={userProfile().hasPassword}>
|
||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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 = ?",
|
||||||
|
|||||||
Reference in New Issue
Block a user