almost done

This commit is contained in:
Michael Freno
2025-12-19 14:04:32 -05:00
parent 0459c9536c
commit 8f4fac422b
6 changed files with 269 additions and 110 deletions

View File

@@ -7,8 +7,7 @@ import {
createResource, createResource,
Show, Show,
For, For,
Suspense, Suspense
createMemo
} from "solid-js"; } from "solid-js";
import { api } from "~/lib/api"; import { api } from "~/lib/api";
import { TerminalSplash } from "./TerminalSplash"; import { TerminalSplash } from "./TerminalSplash";
@@ -57,8 +56,8 @@ export function RightBarContent() {
}); });
return ( return (
<div class="text-text flex h-full w-min flex-col gap-6 overflow-y-auto pb-6"> <div class="text-text flex h-full flex-col gap-6 overflow-y-auto pb-6 md:w-min">
<Typewriter keepAlive={false} class="z-50 px-4 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">Contact Me</a>
@@ -113,7 +112,8 @@ export function RightBarContent() {
{/* Git Activity Section */} {/* Git Activity Section */}
<Suspense fallback={<TerminalSplash />}> <Suspense fallback={<TerminalSplash />}>
<div class="border-overlay0 flex min-w-0 flex-col gap-6 border-t px-4 pt-6"> <hr class="border-overlay0" />
<div class="flex min-w-0 flex-col gap-6 px-4 pt-6">
<RecentCommits <RecentCommits
commits={githubCommits()} commits={githubCommits()}
title="Recent GitHub Commits" title="Recent GitHub Commits"
@@ -405,7 +405,7 @@ export function LeftBar() {
{/* Navigation Links */} {/* Navigation Links */}
<div class="mt-auto"> <div class="mt-auto">
<Typewriter keepAlive={false}> <Typewriter keepAlive={false}>
<ul class="flex flex-row gap-4 py-6 md:flex-col"> <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="/">Home</a>
</li> </li>
@@ -442,13 +442,12 @@ export function LeftBar() {
</ul> </ul>
</Typewriter> </Typewriter>
{/* Dark Mode Toggle */} <hr class="border-overlay0 -mx-4 my-auto" />
<div class="border-overlay0 border-t pt-6"> <div class="my-auto">
<DarkModeToggle /> <DarkModeToggle />
</div> </div>
{/* RightBar content on mobile */} <div class="border-overlay0 -mx-4 border-t pt-8 md:hidden">
<div class="border-overlay0 border-t pt-8 md:hidden">
<RightBarContent /> <RightBarContent />
</div> </div>
</div> </div>

View File

@@ -469,10 +469,10 @@ export default function TextEditor(props: TextEditorProps) {
onClick={() => onClick={() =>
instance().chain().focus().setHorizontalRule().run() instance().chain().focus().setHorizontalRule().run()
} }
class="hover:bg-surface1 rounded px-2 py-1 text-xs" class="bg-surface0 hover:bg-surface1 rounded px-3 py-1 text-xs"
title="Horizontal Rule" title="Horizontal Rule"
> >
HR ━━ HR
</button> </button>
</div> </div>
</> </>
@@ -481,7 +481,7 @@ export default function TextEditor(props: TextEditorProps) {
<div <div
ref={editorRef} ref={editorRef}
class="prose prose-sm prose-invert sm:prose-base md:prose-xl lg:prose-xl xl:prose-2xl mx-auto min-h-[400px] min-w-full focus:outline-none" class="prose prose-sm prose-invert sm:prose-base md:prose-xl lg:prose-xl xl:prose-2xl [&_hr]:border-surface2 mx-auto min-h-[400px] min-w-full focus:outline-none [&_hr]:my-8 [&_hr]:border-t-2"
/> />
</div> </div>
); );

View File

@@ -3,6 +3,9 @@ import { useNavigate, cache, redirect } from "@solidjs/router";
import { getEvent } from "vinxi/http"; import { getEvent } from "vinxi/http";
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 XCircle from "~/components/icons/XCircle";
import Dropzone from "~/components/blog/Dropzone";
import AddImageToS3 from "~/lib/s3upload";
import { validatePassword, isValidEmail } from "~/lib/validation"; import { validatePassword, isValidEmail } from "~/lib/validation";
import { checkAuthStatus } from "~/server/utils"; import { checkAuthStatus } from "~/server/utils";
@@ -71,6 +74,17 @@ export default function AccountPage() {
createSignal(false); createSignal(false);
const [showPasswordSuccess, setShowPasswordSuccess] = createSignal(false); const [showPasswordSuccess, setShowPasswordSuccess] = createSignal(false);
// Profile image state
const [profileImage, setProfileImage] = createSignal<Blob | undefined>(
undefined
);
const [profileImageHolder, setProfileImageHolder] = createSignal<
string | null
>(null);
const [profileImageStateChange, setProfileImageStateChange] =
createSignal(false);
const [preSetHolder, setPreSetHolder] = createSignal<string | null>(null);
// Form refs // Form refs
let oldPasswordRef: HTMLInputElement | undefined; let oldPasswordRef: HTMLInputElement | undefined;
let newPasswordRef: HTMLInputElement | undefined; let newPasswordRef: HTMLInputElement | undefined;
@@ -90,6 +104,10 @@ export default function AccountPage() {
const result = await response.json(); const result = await response.json();
if (result.result?.data) { if (result.result?.data) {
setUser(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) { } catch (err) {
@@ -99,6 +117,82 @@ export default function AccountPage() {
} }
}); });
// Profile image handlers
const handleImageDrop = (acceptedFiles: File[]) => {
acceptedFiles.forEach((file: File) => {
setProfileImage(file);
const reader = new FileReader();
reader.onload = () => {
const str = reader.result as string;
setProfileImageHolder(str);
setProfileImageStateChange(true);
};
reader.readAsDataURL(file);
});
};
const removeImage = () => {
setProfileImage(undefined);
setProfileImageHolder(null);
if (preSetHolder()) {
setProfileImageStateChange(true);
setPreSetHolder(null);
} else {
setProfileImageStateChange(false);
}
};
const setUserImage = async (e: Event) => {
e.preventDefault();
setProfileImageSetLoading(true);
setShowImageSuccess(false);
const currentUser = user();
if (!currentUser) {
setProfileImageSetLoading(false);
return;
}
try {
let imageUrl = "";
// Upload new image if one was selected
if (profileImage()) {
const imageKey = await AddImageToS3(
profileImage()!,
currentUser.id,
"user"
);
imageUrl = imageKey || "";
}
// Update user profile image
const response = await fetch("/api/trpc/user.updateProfileImage", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ imageUrl })
});
const result = await response.json();
if (response.ok && result.result?.data) {
setUser(result.result.data);
setShowImageSuccess(true);
setProfileImageStateChange(false);
setTimeout(() => setShowImageSuccess(false), 3000);
// Update preSetHolder with new image
setPreSetHolder(imageUrl || null);
} else {
alert("Error updating profile image!");
}
} catch (err) {
console.error("Profile image update error:", err);
alert("Error updating profile image! Check console.");
} finally {
setProfileImageSetLoading(false);
}
};
// Email update handler // Email update handler
const setEmailTrigger = async (e: Event) => { const setEmailTrigger = async (e: Event) => {
e.preventDefault(); e.preventDefault();
@@ -375,6 +469,57 @@ export default function AccountPage() {
Account Settings Account Settings
</div> </div>
{/* Profile Image Section */}
<div class="mx-auto mb-8 flex max-w-md justify-center">
<div class="flex flex-col py-4">
<div class="mb-2 text-center text-lg font-semibold">
Profile Image
</div>
<div class="flex items-start">
<Dropzone
onDrop={handleImageDrop}
acceptedFiles="image/jpg, image/jpeg, image/png"
fileHolder={profileImageHolder()}
preSet={preSetHolder() || currentUser().image || null}
/>
<button
type="button"
onClick={removeImage}
class="z-20 -ml-6 h-fit rounded-full transition-all hover:brightness-125"
>
<XCircle
height={36}
width={36}
stroke="currentColor"
strokeWidth={1}
/>
</button>
</div>
<form onSubmit={setUserImage}>
<button
type="submit"
disabled={
profileImageSetLoading() || !profileImageStateChange()
}
class={`${
profileImageSetLoading() || !profileImageStateChange()
? "bg-blue cursor-not-allowed brightness-75"
: "bg-blue hover:brightness-125 active:scale-90"
} mt-2 flex w-full justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
>
{profileImageSetLoading() ? "Uploading..." : "Set Image"}
</button>
</form>
<Show when={showImageSuccess()}>
<div class="text-green mt-2 text-center text-sm">
Profile image updated!
</div>
</Show>
</div>
</div>
<hr class="mx-auto mb-8 max-w-4xl" />
{/* Email Section */} {/* Email Section */}
<div class="mx-auto flex max-w-4xl flex-col gap-6 md:grid md:grid-cols-2"> <div class="mx-auto flex max-w-4xl flex-col gap-6 md:grid md:grid-cols-2">
<div class="flex items-center justify-center text-lg md:justify-normal"> <div class="flex items-center justify-center text-lg md:justify-normal">
@@ -431,7 +576,7 @@ export default function AccountPage() {
emailButtonLoading() || emailButtonLoading() ||
(currentUser().email !== null && (currentUser().email !== null &&
!currentUser().emailVerified) !currentUser().emailVerified)
? "bg-blue cursor-not-allowed brightness-50" ? "bg-blue cursor-not-allowed brightness-75"
: "bg-blue hover:brightness-125 active:scale-90" : "bg-blue hover:brightness-125 active:scale-90"
} mt-2 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`} } mt-2 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
> >
@@ -482,7 +627,7 @@ export default function AccountPage() {
disabled={displayNameButtonLoading()} disabled={displayNameButtonLoading()}
class={`${ class={`${
displayNameButtonLoading() displayNameButtonLoading()
? "bg-blue cursor-not-allowed brightness-50" ? "bg-blue cursor-not-allowed brightness-75"
: "bg-blue hover:brightness-125 active:scale-90" : "bg-blue hover:brightness-125 active:scale-90"
} mt-2 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`} } mt-2 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
> >
@@ -610,7 +755,7 @@ export default function AccountPage() {
disabled={passwordChangeLoading() || !passwordsMatch()} disabled={passwordChangeLoading() || !passwordsMatch()}
class={`${ class={`${
passwordChangeLoading() || !passwordsMatch() passwordChangeLoading() || !passwordsMatch()
? "bg-blue cursor-not-allowed brightness-50" ? "bg-blue cursor-not-allowed brightness-75"
: "bg-blue hover:brightness-125 active:scale-90" : "bg-blue hover:brightness-125 active:scale-90"
} my-6 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`} } my-6 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
> >
@@ -670,7 +815,7 @@ export default function AccountPage() {
disabled={deleteAccountButtonLoading()} disabled={deleteAccountButtonLoading()}
class={`${ class={`${
deleteAccountButtonLoading() deleteAccountButtonLoading()
? "bg-red cursor-not-allowed brightness-50" ? "bg-red cursor-not-allowed brightness-75"
: "bg-red hover:brightness-125 active:scale-90" : "bg-red hover:brightness-125 active:scale-90"
} mx-auto mt-4 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`} } mx-auto mt-4 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
> >

View File

@@ -105,15 +105,14 @@ export default function ContactPage() {
<div class="w-full py-12"> <div class="w-full py-12">
<RevealDropDown title={"Questions about Life and Lineage?"}> <RevealDropDown title={"Questions about Life and Lineage?"}>
<div> <div>
Feel free to use the form{" "} Feel free to use the form below, I will respond as quickly as
{viewer() === "lineage" ? "below" : "above"}, I will respond as possible, however, you may find an answer to your question in the
quickly as possible, however, you may find an answer to your following.
question in the following.
</div> </div>
<ol> <ol>
<div class="py-2"> <div class="py-2">
<div class="pb-2 text-lg"> <div class="pb-2 text-lg">
<span class="-ml-4 pr-2">1.</span> Personal Information <span class="-ml-2 pr-2">1.</span> Personal Information
</div> </div>
<div class="pl-4"> <div class="pl-4">
<div class="pb-2"> <div class="pb-2">
@@ -130,7 +129,7 @@ export default function ContactPage() {
</div> </div>
<div class="py-2"> <div class="py-2">
<div class="pb-2 text-lg"> <div class="pb-2 text-lg">
<span class="-ml-4 pr-2">2.</span> Remote Backups <span class="-ml-2 pr-2">2.</span> Remote Backups
</div> </div>
<div class="pl-4"> <div class="pl-4">
<em>Life and Lineage</em> uses a per-user database approach for <em>Life and Lineage</em> uses a per-user database approach for
@@ -146,7 +145,7 @@ export default function ContactPage() {
</div> </div>
<div class="py-2"> <div class="py-2">
<div class="pb-2 text-lg"> <div class="pb-2 text-lg">
<span class="-ml-4 pr-2">3.</span> Cross Device Play <span class="-ml-2 pr-2">3.</span> Cross Device Play
</div> </div>
<div class="pl-4"> <div class="pl-4">
You can use the above mentioned remote-backups to save progress You can use the above mentioned remote-backups to save progress
@@ -155,7 +154,7 @@ export default function ContactPage() {
</div> </div>
<div class="py-2"> <div class="py-2">
<div class="pb-2 text-lg"> <div class="pb-2 text-lg">
<span class="-ml-4 pr-2">4.</span> Online Requirements <span class="-ml-2 pr-2">4.</span> Online Requirements
</div> </div>
<div class="pl-4"> <div class="pl-4">
Currently, the only time you need to be online is for remote Currently, the only time you need to be online is for remote
@@ -166,7 +165,7 @@ export default function ContactPage() {
</div> </div>
<div class="py-2"> <div class="py-2">
<div class="pb-2 text-lg"> <div class="pb-2 text-lg">
<span class="-ml-4 pr-2">5.</span> Microtransactions <span class="-ml-2 pr-2">5.</span> Microtransactions
</div> </div>
<div class="pl-4"> <div class="pl-4">
Microtransactions are not required to play or complete the game, Microtransactions are not required to play or complete the game,
@@ -205,9 +204,7 @@ export default function ContactPage() {
(for this website or any of my apps...) (for this website or any of my apps...)
</div> </div>
</Show> </Show>
<Show when={viewer() === "lineage"}> <LineageQuestionsDropDown />
<LineageQuestionsDropDown />
</Show>
<form onSubmit={sendEmailTrigger} class="w-full"> <form onSubmit={sendEmailTrigger} class="w-full">
<div <div
class={`flex w-full flex-col justify-evenly pt-6 ${ class={`flex w-full flex-col justify-evenly pt-6 ${
@@ -286,9 +283,6 @@ export default function ContactPage() {
</div> </div>
</div> </div>
</form> </form>
<Show when={viewer() !== "lineage"}>
<LineageQuestionsDropDown />
</Show>
<div <div
class={`${ class={`${
emailSent() emailSent()

View File

@@ -2,38 +2,51 @@ import { Typewriter } from "~/components/Typewriter";
export default function Home() { export default function Home() {
return ( return (
<main class="p-4 text-xl"> <main class="flex h-full flex-col p-4 text-xl">
<Typewriter speed={30} keepAlive={2000}> <div class="flex-1">
<div class="text-4xl">Hey!</div> <Typewriter speed={30} keepAlive={2000}>
</Typewriter> <div class="text-4xl">Hey!</div>
<Typewriter speed={80} keepAlive={2000}> </Typewriter>
<div> <Typewriter speed={80} keepAlive={2000}>
My name is <span class="text-green">Mike Freno</span>, I'm a{" "} <div>
<span class="text-blue">Software Engineer</span> based in{" "} My name is <span class="text-green">Mike Freno</span>, I'm a{" "}
<span class="text-yellow">Brooklyn, NY</span> <span class="text-blue">Software Engineer</span> based in{" "}
</div> <span class="text-yellow">Brooklyn, NY</span>
</Typewriter> </div>
<Typewriter speed={100}> </Typewriter>
I'm a passionate dev tooling, game, and open source software developer. <Typewriter speed={100} keepAlive={2000}>
Recently been working in the world of{" "} I'm a passionate dev tooling, game, and open source software
<a developer. Recently been working in the world of{" "}
href="https://www.love2d.org" <a
class="text-blue hover-underline-animation" href="https://www.love2d.org"
> class="text-blue hover-underline-animation"
LÖVE >
</a>{" "} LÖVE
</Typewriter> </a>{" "}
You can see some of my work <a>here</a>(github) </Typewriter>
<Typewriter speed={50} keepAlive={false}>
<div>My Collection of By-the-ways:</div> <Typewriter speed={100} keepAlive={2000}>
</Typewriter> You can see some of my work{" "}
<Typewriter speed={50} keepAlive={false}> <a
<ul class="list-disc pl-8"> href="https://github.com/mikefreno"
<li>I use Neovim</li> class="text-blue hover-underline-animation"
<li>I use Arch Linux</li> >
<li>I use Rust</li> here (github)
</ul> </a>
</Typewriter> </Typewriter>
</div>
<div class="flex flex-col items-end gap-4">
<Typewriter speed={50} keepAlive={false}>
<div>My Collection of By-the-ways:</div>
</Typewriter>
<Typewriter speed={50} keepAlive={false}>
<ul class="list-disc pl-8">
<li>I use Neovim</li>
<li>I use Arch Linux</li>
<li>I use Rust</li>
</ul>
</Typewriter>
</div>
</main> </main>
); );
} }

View File

@@ -2,7 +2,12 @@ import { createTRPCRouter, publicProcedure } from "../utils";
import { z } from "zod"; import { z } from "zod";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { env } from "~/env/server"; import { env } from "~/env/server";
import { ConnectionFactory, getUserID, hashPassword, checkPassword } from "~/server/utils"; import {
ConnectionFactory,
getUserID,
hashPassword,
checkPassword
} from "~/server/utils";
import { setCookie } from "vinxi/http"; import { setCookie } from "vinxi/http";
import type { User } from "~/types/user"; import type { User } from "~/types/user";
import { toUserProfile } from "~/types/user"; import { toUserProfile } from "~/types/user";
@@ -15,20 +20,20 @@ export const userRouter = createTRPCRouter({
if (!userId) { if (!userId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Not authenticated", message: "Not authenticated"
}); });
} }
const conn = ConnectionFactory(); const conn = ConnectionFactory();
const res = await conn.execute({ const res = await conn.execute({
sql: "SELECT * FROM User WHERE id = ?", sql: "SELECT * FROM User WHERE id = ?",
args: [userId], args: [userId]
}); });
if (res.rows.length === 0) { if (res.rows.length === 0) {
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "User not found", message: "User not found"
}); });
} }
@@ -45,7 +50,7 @@ export const userRouter = createTRPCRouter({
if (!userId) { if (!userId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Not authenticated", message: "Not authenticated"
}); });
} }
@@ -54,20 +59,20 @@ export const userRouter = createTRPCRouter({
await conn.execute({ await conn.execute({
sql: "UPDATE User SET email = ?, email_verified = ? WHERE id = ?", sql: "UPDATE User SET email = ?, email_verified = ? WHERE id = ?",
args: [email, 0, userId], args: [email, 0, userId]
}); });
// Fetch updated user // Fetch updated user
const res = await conn.execute({ const res = await conn.execute({
sql: "SELECT * FROM User WHERE id = ?", sql: "SELECT * FROM User WHERE id = ?",
args: [userId], args: [userId]
}); });
const user = res.rows[0] as unknown as User; const user = res.rows[0] as unknown as User;
// Set email cookie for verification flow // Set email cookie for verification flow
setCookie(ctx.event.nativeEvent, "emailToken", email, { setCookie(ctx.event.nativeEvent, "emailToken", email, {
path: "/", path: "/"
}); });
return toUserProfile(user); return toUserProfile(user);
@@ -82,7 +87,7 @@ export const userRouter = createTRPCRouter({
if (!userId) { if (!userId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Not authenticated", message: "Not authenticated"
}); });
} }
@@ -91,13 +96,13 @@ export const userRouter = createTRPCRouter({
await conn.execute({ await conn.execute({
sql: "UPDATE User SET display_name = ? WHERE id = ?", sql: "UPDATE User SET display_name = ? WHERE id = ?",
args: [displayName, userId], args: [displayName, userId]
}); });
// Fetch updated user // Fetch updated user
const res = await conn.execute({ const res = await conn.execute({
sql: "SELECT * FROM User WHERE id = ?", sql: "SELECT * FROM User WHERE id = ?",
args: [userId], args: [userId]
}); });
const user = res.rows[0] as unknown as User; const user = res.rows[0] as unknown as User;
@@ -106,14 +111,14 @@ export const userRouter = createTRPCRouter({
// Update profile image // Update profile image
updateProfileImage: publicProcedure updateProfileImage: publicProcedure
.input(z.object({ imageUrl: z.string().url() })) .input(z.object({ imageUrl: z.string() }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const userId = await getUserID(ctx.event.nativeEvent); const userId = await getUserID(ctx.event.nativeEvent);
if (!userId) { if (!userId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Not authenticated", message: "Not authenticated"
}); });
} }
@@ -122,13 +127,13 @@ export const userRouter = createTRPCRouter({
await conn.execute({ await conn.execute({
sql: "UPDATE User SET image = ? WHERE id = ?", sql: "UPDATE User SET image = ? WHERE id = ?",
args: [imageUrl, userId], args: [imageUrl, userId]
}); });
// Fetch updated user // Fetch updated user
const res = await conn.execute({ const res = await conn.execute({
sql: "SELECT * FROM User WHERE id = ?", sql: "SELECT * FROM User WHERE id = ?",
args: [userId], args: [userId]
}); });
const user = res.rows[0] as unknown as User; const user = res.rows[0] as unknown as User;
@@ -141,8 +146,8 @@ export const userRouter = createTRPCRouter({
z.object({ z.object({
oldPassword: z.string(), oldPassword: z.string(),
newPassword: z.string().min(8), newPassword: z.string().min(8),
newPasswordConfirmation: z.string().min(8), newPasswordConfirmation: z.string().min(8)
}), })
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const userId = await getUserID(ctx.event.nativeEvent); const userId = await getUserID(ctx.event.nativeEvent);
@@ -150,7 +155,7 @@ export const userRouter = createTRPCRouter({
if (!userId) { if (!userId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Not authenticated", message: "Not authenticated"
}); });
} }
@@ -159,20 +164,20 @@ export const userRouter = createTRPCRouter({
if (newPassword !== newPasswordConfirmation) { if (newPassword !== newPasswordConfirmation) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Password Mismatch", message: "Password Mismatch"
}); });
} }
const conn = ConnectionFactory(); const conn = ConnectionFactory();
const res = await conn.execute({ const res = await conn.execute({
sql: "SELECT * FROM User WHERE id = ?", sql: "SELECT * FROM User WHERE id = ?",
args: [userId], args: [userId]
}); });
if (res.rows.length === 0) { if (res.rows.length === 0) {
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "User not found", message: "User not found"
}); });
} }
@@ -181,16 +186,19 @@ export const userRouter = createTRPCRouter({
if (!user.password_hash) { if (!user.password_hash) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "No password set", message: "No password set"
}); });
} }
const passwordMatch = await checkPassword(oldPassword, user.password_hash); const passwordMatch = await checkPassword(
oldPassword,
user.password_hash
);
if (!passwordMatch) { if (!passwordMatch) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Password did not match record", message: "Password did not match record"
}); });
} }
@@ -198,17 +206,17 @@ export const userRouter = createTRPCRouter({
const newPasswordHash = await hashPassword(newPassword); const newPasswordHash = 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 = ?",
args: [newPasswordHash, userId], args: [newPasswordHash, userId]
}); });
// Clear session cookies (force re-login) // Clear session cookies (force re-login)
setCookie(ctx.event.nativeEvent, "emailToken", "", { setCookie(ctx.event.nativeEvent, "emailToken", "", {
maxAge: 0, maxAge: 0,
path: "/", path: "/"
}); });
setCookie(ctx.event.nativeEvent, "userIDToken", "", { setCookie(ctx.event.nativeEvent, "userIDToken", "", {
maxAge: 0, maxAge: 0,
path: "/", path: "/"
}); });
return { success: true, message: "success" }; return { success: true, message: "success" };
@@ -219,8 +227,8 @@ export const userRouter = createTRPCRouter({
.input( .input(
z.object({ z.object({
newPassword: z.string().min(8), newPassword: z.string().min(8),
newPasswordConfirmation: z.string().min(8), newPasswordConfirmation: z.string().min(8)
}), })
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const userId = await getUserID(ctx.event.nativeEvent); const userId = await getUserID(ctx.event.nativeEvent);
@@ -228,7 +236,7 @@ export const userRouter = createTRPCRouter({
if (!userId) { if (!userId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Not authenticated", message: "Not authenticated"
}); });
} }
@@ -237,20 +245,20 @@ export const userRouter = createTRPCRouter({
if (newPassword !== newPasswordConfirmation) { if (newPassword !== newPasswordConfirmation) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Password Mismatch", message: "Password Mismatch"
}); });
} }
const conn = ConnectionFactory(); const conn = ConnectionFactory();
const res = await conn.execute({ const res = await conn.execute({
sql: "SELECT * FROM User WHERE id = ?", sql: "SELECT * FROM User WHERE id = ?",
args: [userId], args: [userId]
}); });
if (res.rows.length === 0) { if (res.rows.length === 0) {
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "User not found", message: "User not found"
}); });
} }
@@ -259,7 +267,7 @@ export const userRouter = createTRPCRouter({
if (user.password_hash) { if (user.password_hash) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Password exists", message: "Password exists"
}); });
} }
@@ -267,17 +275,17 @@ export const userRouter = createTRPCRouter({
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 = ?",
args: [passwordHash, userId], args: [passwordHash, userId]
}); });
// Clear session cookies (force re-login) // Clear session cookies (force re-login)
setCookie(ctx.event.nativeEvent, "emailToken", "", { setCookie(ctx.event.nativeEvent, "emailToken", "", {
maxAge: 0, maxAge: 0,
path: "/", path: "/"
}); });
setCookie(ctx.event.nativeEvent, "userIDToken", "", { setCookie(ctx.event.nativeEvent, "userIDToken", "", {
maxAge: 0, maxAge: 0,
path: "/", path: "/"
}); });
return { success: true, message: "success" }; return { success: true, message: "success" };
@@ -292,7 +300,7 @@ export const userRouter = createTRPCRouter({
if (!userId) { if (!userId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Not authenticated", message: "Not authenticated"
}); });
} }
@@ -301,13 +309,13 @@ export const userRouter = createTRPCRouter({
const res = await conn.execute({ const res = await conn.execute({
sql: "SELECT * FROM User WHERE id = ?", sql: "SELECT * FROM User WHERE id = ?",
args: [userId], args: [userId]
}); });
if (res.rows.length === 0) { if (res.rows.length === 0) {
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "User not found", message: "User not found"
}); });
} }
@@ -316,7 +324,7 @@ export const userRouter = createTRPCRouter({
if (!user.password_hash) { if (!user.password_hash) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Password required", message: "Password required"
}); });
} }
@@ -325,7 +333,7 @@ export const userRouter = createTRPCRouter({
if (!passwordMatch) { if (!passwordMatch) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Password Did Not Match", message: "Password Did Not Match"
}); });
} }
@@ -339,19 +347,19 @@ export const userRouter = createTRPCRouter({
provider = ?, provider = ?,
image = ? image = ?
WHERE id = ?`, WHERE id = ?`,
args: [null, 0, null, "user deleted", null, null, userId], args: [null, 0, null, "user deleted", null, null, userId]
}); });
// Clear session cookies // Clear session cookies
setCookie(ctx.event.nativeEvent, "emailToken", "", { setCookie(ctx.event.nativeEvent, "emailToken", "", {
maxAge: 0, maxAge: 0,
path: "/", path: "/"
}); });
setCookie(ctx.event.nativeEvent, "userIDToken", "", { setCookie(ctx.event.nativeEvent, "userIDToken", "", {
maxAge: 0, maxAge: 0,
path: "/", path: "/"
}); });
return { success: true, message: "deleted" }; return { success: true, message: "deleted" };
}), })
}); });