flow update
This commit is contained in:
23
src/components/icons/EmailIcon.tsx
Normal file
23
src/components/icons/EmailIcon.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Component } from "solid-js";
|
||||
|
||||
interface EmailIconProps {
|
||||
height?: string | number;
|
||||
width?: string | number;
|
||||
fill?: string;
|
||||
}
|
||||
|
||||
const EmailIcon: Component<EmailIconProps> = (props) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
height={props.height}
|
||||
width={props.width}
|
||||
fill={props.fill || "currentColor"}
|
||||
>
|
||||
<path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48H48zM0 176V384c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V176L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailIcon;
|
||||
@@ -5,6 +5,9 @@ import { getEvent } from "vinxi/http";
|
||||
import Eye from "~/components/icons/Eye";
|
||||
import EyeSlash from "~/components/icons/EyeSlash";
|
||||
import XCircle from "~/components/icons/XCircle";
|
||||
import GoogleLogo from "~/components/icons/GoogleLogo";
|
||||
import GitHub from "~/components/icons/GitHub";
|
||||
import EmailIcon from "~/components/icons/EmailIcon";
|
||||
import Dropzone from "~/components/blog/Dropzone";
|
||||
import AddImageToS3 from "~/lib/s3upload";
|
||||
import { validatePassword, isValidEmail } from "~/lib/validation";
|
||||
@@ -16,7 +19,7 @@ type UserProfile = {
|
||||
emailVerified: boolean;
|
||||
displayName: string | null;
|
||||
image: string | null;
|
||||
provider: string;
|
||||
provider: "email" | "google" | "github" | null;
|
||||
hasPassword: boolean;
|
||||
};
|
||||
|
||||
@@ -454,6 +457,36 @@ export default function AccountPage() {
|
||||
setPasswordBlurred(true);
|
||||
};
|
||||
|
||||
// Helper to get provider display info
|
||||
const getProviderInfo = (provider: UserProfile["provider"]) => {
|
||||
switch (provider) {
|
||||
case "google":
|
||||
return {
|
||||
name: "Google",
|
||||
icon: <GoogleLogo height={24} width={24} />,
|
||||
color: "text-blue-500"
|
||||
};
|
||||
case "github":
|
||||
return {
|
||||
name: "GitHub",
|
||||
icon: <GitHub height={24} width={24} fill="currentColor" />,
|
||||
color: "text-gray-700 dark:text-gray-300"
|
||||
};
|
||||
case "email":
|
||||
return {
|
||||
name: "Email",
|
||||
icon: <EmailIcon height={24} width={24} />,
|
||||
color: "text-green-500"
|
||||
};
|
||||
default:
|
||||
return {
|
||||
name: "Unknown",
|
||||
icon: <EmailIcon height={24} width={24} />,
|
||||
color: "text-gray-500"
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title>Account | Michael Freno</Title>
|
||||
@@ -471,6 +504,47 @@ export default function AccountPage() {
|
||||
Account Settings
|
||||
</div>
|
||||
|
||||
{/* Account Type Section */}
|
||||
<div class="mx-auto mb-8 max-w-md">
|
||||
<div class="bg-surface0 border-surface1 rounded-lg border px-6 py-4 shadow-sm">
|
||||
<div class="text-subtext0 mb-2 text-center text-sm font-semibold tracking-wide uppercase">
|
||||
Account Type
|
||||
</div>
|
||||
<div class="flex items-center justify-center gap-3">
|
||||
<span
|
||||
class={getProviderInfo(currentUser().provider).color}
|
||||
>
|
||||
{getProviderInfo(currentUser().provider).icon}
|
||||
</span>
|
||||
<span class="text-lg font-semibold">
|
||||
{getProviderInfo(currentUser().provider).name} Account
|
||||
</span>
|
||||
</div>
|
||||
<Show
|
||||
when={
|
||||
currentUser().provider !== "email" &&
|
||||
!currentUser().email
|
||||
}
|
||||
>
|
||||
<div class="mt-3 rounded bg-yellow-500/10 px-3 py-2 text-center text-sm text-yellow-600 dark:text-yellow-400">
|
||||
⚠️ Add an email address for account recovery
|
||||
</div>
|
||||
</Show>
|
||||
<Show
|
||||
when={
|
||||
currentUser().provider !== "email" &&
|
||||
!currentUser().hasPassword
|
||||
}
|
||||
>
|
||||
<div class="mt-3 rounded bg-blue-500/10 px-3 py-2 text-center text-sm text-blue-600 dark:text-blue-400">
|
||||
💡 Add a password to enable email/password login
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="mx-auto mb-8 max-w-4xl" />
|
||||
|
||||
{/* Profile Image Section */}
|
||||
<div class="mx-auto mb-8 flex max-w-md justify-center">
|
||||
<div class="flex flex-col py-4">
|
||||
@@ -529,13 +603,17 @@ export default function AccountPage() {
|
||||
<div class="flex items-center justify-center text-lg md:justify-normal">
|
||||
<div class="flex flex-col lg:flex-row">
|
||||
<div class="pr-1 font-semibold whitespace-nowrap">
|
||||
Current email:
|
||||
{currentUser().provider === "email"
|
||||
? "Email:"
|
||||
: "Linked Email:"}
|
||||
</div>
|
||||
{currentUser().email ? (
|
||||
<span>{currentUser().email}</span>
|
||||
) : (
|
||||
<span class="font-light italic underline underline-offset-4">
|
||||
None Set
|
||||
{currentUser().provider === "email"
|
||||
? "None Set"
|
||||
: "Not Linked"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -566,8 +644,20 @@ export default function AccountPage() {
|
||||
class="underlinedInput bg-transparent"
|
||||
/>
|
||||
<span class="bar"></span>
|
||||
<label class="underlinedInputLabel">Set New Email</label>
|
||||
<label class="underlinedInputLabel">
|
||||
{currentUser().email ? "Update Email" : "Add Email"}
|
||||
</label>
|
||||
</div>
|
||||
<Show
|
||||
when={
|
||||
currentUser().provider !== "email" &&
|
||||
!currentUser().email
|
||||
}
|
||||
>
|
||||
<div class="text-subtext0 mt-1 px-4 text-xs">
|
||||
Add an email for account recovery and notifications
|
||||
</div>
|
||||
</Show>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
@@ -655,11 +745,20 @@ export default function AccountPage() {
|
||||
class="mt-8 flex w-full justify-center"
|
||||
>
|
||||
<div class="flex w-full max-w-md flex-col justify-center">
|
||||
<div class="mb-4 text-center text-xl font-semibold">
|
||||
<div class="mb-2 text-center text-xl font-semibold">
|
||||
{currentUser().hasPassword
|
||||
? "Change Password"
|
||||
: "Set Password"}
|
||||
: "Add Password"}
|
||||
</div>
|
||||
<Show when={!currentUser().hasPassword}>
|
||||
<div class="text-subtext0 mb-4 text-center text-sm">
|
||||
{currentUser().provider === "email"
|
||||
? "Set a password to enable password login"
|
||||
: "Add a password to enable email/password login alongside your " +
|
||||
getProviderInfo(currentUser().provider).name +
|
||||
" login"}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={currentUser().hasPassword}>
|
||||
<div class="input-group relative mx-4 mb-6">
|
||||
@@ -804,6 +903,27 @@ export default function AccountPage() {
|
||||
irreversible
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={currentUser().hasPassword}
|
||||
fallback={
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="text-crust mb-4 text-center text-sm">
|
||||
Your {getProviderInfo(currentUser().provider).name}{" "}
|
||||
account doesn't have a password. To delete your
|
||||
account, please set a password first, then return
|
||||
here to proceed with deletion.
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}}
|
||||
class="bg-surface0 hover:bg-surface1 rounded px-4 py-2 transition-all"
|
||||
>
|
||||
Go to Add Password Section
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<form onSubmit={deleteAccountTrigger}>
|
||||
<div class="flex w-full justify-center">
|
||||
<div class="input-group delete mx-4">
|
||||
@@ -842,6 +962,7 @@ export default function AccountPage() {
|
||||
</div>
|
||||
</Show>
|
||||
</form>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -117,6 +117,30 @@ export const authRouter = createTRPCRouter({
|
||||
await checkResponse(userResponse);
|
||||
const user = await userResponse.json();
|
||||
const login = user.login;
|
||||
const icon = user.avatar_url;
|
||||
|
||||
// Fetch primary email from GitHub emails endpoint
|
||||
const emailsResponse = await fetchWithTimeout(
|
||||
"https://api.github.com/user/emails",
|
||||
{
|
||||
headers: {
|
||||
Authorization: `token ${access_token}`
|
||||
},
|
||||
timeout: 15000
|
||||
}
|
||||
);
|
||||
|
||||
await checkResponse(emailsResponse);
|
||||
const emails = await emailsResponse.json();
|
||||
|
||||
// Find primary verified email
|
||||
const primaryEmail = emails.find(
|
||||
(e: { primary: boolean; verified: boolean; email: string }) =>
|
||||
e.primary && e.verified
|
||||
);
|
||||
const email = primaryEmail?.email || null;
|
||||
const emailVerified = primaryEmail?.verified || false;
|
||||
|
||||
const conn = ConnectionFactory();
|
||||
|
||||
// Check if user exists
|
||||
@@ -127,16 +151,26 @@ export const authRouter = createTRPCRouter({
|
||||
let userId: string;
|
||||
|
||||
if (res.rows[0]) {
|
||||
// User exists
|
||||
// User exists - update email and image if changed
|
||||
userId = (res.rows[0] as unknown as User).id;
|
||||
|
||||
await conn.execute({
|
||||
sql: `UPDATE User SET email = ?, email_verified = ?, image = ? WHERE id = ?`,
|
||||
args: [email, emailVerified ? 1 : 0, icon, userId]
|
||||
});
|
||||
} else {
|
||||
// Create new user
|
||||
const icon = user.avatar_url;
|
||||
const email = user.email;
|
||||
userId = uuidV4();
|
||||
|
||||
const insertQuery = `INSERT INTO User (id, email, display_name, provider, image) VALUES (?, ?, ?, ?, ?)`;
|
||||
const insertParams = [userId, email, login, "github", icon];
|
||||
const insertQuery = `INSERT INTO User (id, email, email_verified, display_name, provider, image) VALUES (?, ?, ?, ?, ?, ?)`;
|
||||
const insertParams = [
|
||||
userId,
|
||||
email,
|
||||
emailVerified ? 1 : 0,
|
||||
login,
|
||||
"github",
|
||||
icon
|
||||
];
|
||||
await conn.execute({ sql: insertQuery, args: insertParams });
|
||||
}
|
||||
|
||||
@@ -254,8 +288,13 @@ export const authRouter = createTRPCRouter({
|
||||
let userId: string;
|
||||
|
||||
if (res.rows[0]) {
|
||||
// User exists
|
||||
// User exists - update email, email_verified, display_name, and image if changed
|
||||
userId = (res.rows[0] as unknown as User).id;
|
||||
|
||||
await conn.execute({
|
||||
sql: `UPDATE User SET email = ?, email_verified = ?, display_name = ?, image = ? WHERE id = ?`,
|
||||
args: [email, email_verified ? 1 : 0, name, image, userId]
|
||||
});
|
||||
} else {
|
||||
// Create new user
|
||||
userId = uuidV4();
|
||||
@@ -264,7 +303,7 @@ export const authRouter = createTRPCRouter({
|
||||
const insertParams = [
|
||||
userId,
|
||||
email,
|
||||
email_verified,
|
||||
email_verified ? 1 : 0,
|
||||
name,
|
||||
"google",
|
||||
image
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface User {
|
||||
email_verified: number; // SQLite boolean (0 or 1)
|
||||
password_hash: string | null;
|
||||
display_name: string | null;
|
||||
provider: "email" | "google" | "apple" | null;
|
||||
provider: "email" | "google" | "github" | null;
|
||||
image: string | null;
|
||||
apple_user_string: string | null;
|
||||
database_name: string | null;
|
||||
@@ -27,7 +27,7 @@ export interface UserProfile {
|
||||
email?: string;
|
||||
emailVerified: boolean;
|
||||
displayName?: string;
|
||||
provider?: "email" | "google" | "apple";
|
||||
provider?: "email" | "google" | "github";
|
||||
image?: string;
|
||||
hasPassword: boolean;
|
||||
}
|
||||
@@ -43,7 +43,7 @@ export function toUserProfile(user: User): UserProfile {
|
||||
displayName: user.display_name ?? undefined,
|
||||
provider: user.provider ?? undefined,
|
||||
image: user.image ?? undefined,
|
||||
hasPassword: !!user.password_hash,
|
||||
hasPassword: !!user.password_hash
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user