diff --git a/src/components/icons/EmailIcon.tsx b/src/components/icons/EmailIcon.tsx new file mode 100644 index 0000000..0db18b9 --- /dev/null +++ b/src/components/icons/EmailIcon.tsx @@ -0,0 +1,23 @@ +import { Component } from "solid-js"; + +interface EmailIconProps { + height?: string | number; + width?: string | number; + fill?: string; +} + +const EmailIcon: Component = (props) => { + return ( + + + + ); +}; + +export default EmailIcon; diff --git a/src/routes/account.tsx b/src/routes/account.tsx index 5355bc6..e9adb98 100644 --- a/src/routes/account.tsx +++ b/src/routes/account.tsx @@ -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: , + color: "text-blue-500" + }; + case "github": + return { + name: "GitHub", + icon: , + color: "text-gray-700 dark:text-gray-300" + }; + case "email": + return { + name: "Email", + icon: , + color: "text-green-500" + }; + default: + return { + name: "Unknown", + icon: , + color: "text-gray-500" + }; + } + }; + return ( <> Account | Michael Freno @@ -471,6 +504,47 @@ export default function AccountPage() { Account Settings + {/* Account Type Section */} +
+
+
+ Account Type +
+
+ + {getProviderInfo(currentUser().provider).icon} + + + {getProviderInfo(currentUser().provider).name} Account + +
+ +
+ ⚠️ Add an email address for account recovery +
+
+ +
+ 💡 Add a password to enable email/password login +
+
+
+
+ +
+ {/* Profile Image Section */}
@@ -529,13 +603,17 @@ export default function AccountPage() {
- Current email: + {currentUser().provider === "email" + ? "Email:" + : "Linked Email:"}
{currentUser().email ? ( {currentUser().email} ) : ( - None Set + {currentUser().provider === "email" + ? "None Set" + : "Not Linked"} )}
@@ -566,8 +644,20 @@ export default function AccountPage() { class="underlinedInput bg-transparent" /> - +
+ +
+ Add an email for account recovery and notifications +
+
-
- - - - -
- Password did not match record + } + > + +
+
+ + + +
- - + + + + +
+ Password did not match record +
+
+ +
diff --git a/src/server/api/routers/auth.ts b/src/server/api/routers/auth.ts index 0dc964a..e1efd01 100644 --- a/src/server/api/routers/auth.ts +++ b/src/server/api/routers/auth.ts @@ -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 diff --git a/src/types/user.ts b/src/types/user.ts index 1250484..bce9ab4 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -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 }; }