From 83e3f867dd66d63e3f3a67bf618b075a2a928b3d Mon Sep 17 00:00:00 2001 From: Avraham Sakal Date: Thu, 18 Sep 2025 10:05:20 -0400 Subject: [PATCH] can sign in and out --- assets/google-logo.webp | Bin 0 -> 892 bytes database/common.ts | 10 ++++ database/postgres.ts | 52 ++++++++++++++++++++ layouts/LayoutDefault.tsx | 70 ++++++++++++++++++++++++++- pages/+onCreatePageContext.server.ts | 22 +++++++++ server/authjs-handler.ts | 62 +++++++++++++++++++----- 6 files changed, 204 insertions(+), 12 deletions(-) create mode 100644 assets/google-logo.webp create mode 100644 pages/+onCreatePageContext.server.ts diff --git a/assets/google-logo.webp b/assets/google-logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..b57d89bcdecbcbb2153c1fb0c0351c3fe2933a2a GIT binary patch literal 892 zcmV-?1B3ihNk&F=0{{S5MM6+kP&il$0000G0000l001ul06|PpNOu7M00EG5ZMSK= z(r0;Xo829^v~Al~D%-a0R7q#`wr$&XX0+#=Yes}4w~ZW$#?0nDRp>W>Fb;v?L5`pl zxBjh9@&CNsJu8z9Xp)H%XVEPCd5_T0Al5z8i~sX| zrdJ<(4M)b>B2NHt&tLK5e?9Ewi=^-EK&AEPU)^T^z8Sde#SZ>TC$C=M6A!#gly=Za zBy}wZ70U?ELC1SSst!Nmx&u0zrNtIiS1n0>2*5&uD{U^yG8bTv)n}kh6WRiB#_G6= zqX0Lo_Nch3?w0zCtK?U{s5oZuN3(>s8`Mep#x+D1|FW7#*WqoD2d$c!I!_ksAOtgg z^i1r&)larhmkKpdLxrCMZ6eM{HKt(B^a$8|AiNjNu}!R4q%uHZPWtae%16^V><~Zt zBGSpxnDex)N_#|J6pxI3w^iPQpPZHV>;>gLj^mZj347gC&mp+dNAaBV`M|VTU}JKQ zasg2Q09H^qAl3l@0FVp-odGH^05AYPZ8VigBqE|ABZ>fk4T)?3u_Zx%0r-K>seyQ( z@fqm><^%Q<^}F7y&Y7Sm%t1q*JCOCM)I492pfjK`#5GS&hs%x=wO&E z^qcRmyZ`&EdmGlLVZ5vY?qf#S2VDN)^H1f)HdE0KE&IEX&JAx^ZMv`n-dZA`TIOT_ zPlr`|+r-bS;@4lqW)CUCe-^j@`6d(mFiMViaDwjK0Kdvtx8Z^`ER-&l*mCoaEAt`a z5QhB+Kw8*0IsCSN|Ke*^wcc2X_2x3b*w!`i^%GZV$-vD~&o) & { + id: string; +}; + export interface Entity { construct: (data: T) => T; create: (data: Omit) => Promise; @@ -54,9 +59,14 @@ export type FactTriggerEntity = Entity & { findByConversationId: (conversationId: string) => Promise>; }; +export type UserEntity = Entity & { + findByEmailAddress: (emailAddress: string) => Promise; +}; + export interface ApplicationDatabase { conversations: ConversationEntity; factTriggers: FactTriggerEntity; facts: FactEntity; messages: MessageEntity; + users: UserEntity; } diff --git a/database/postgres.ts b/database/postgres.ts index 4fae4a0..85382a2 100644 --- a/database/postgres.ts +++ b/database/postgres.ts @@ -8,6 +8,7 @@ import type { FactEntity, FactTriggerEntity, MessageEntity, + UserEntity, } from "./common.ts"; import type { CommittedMessage } from "../types"; @@ -262,11 +263,62 @@ export function getDb(POSTGRES_CONNECTION_STRING: string) { }, }; + const users: UserEntity = { + construct: (user) => user, + create: async (user) => { + const insertedRows = await dbClient + .insertInto("users") + .values(user) + .returningAll() + .execute(); + return insertedRows[0]; + }, + createMany: async (users) => { + const insertedRows = await dbClient + .insertInto("users") + .values(users) + .returningAll() + .execute(); + return insertedRows; + }, + findAll: async () => { + const rows = await dbClient.selectFrom("users").selectAll().execute(); + return rows; + }, + findById: async (id) => { + const row = await dbClient + .selectFrom("users") + .selectAll() + .where("id", "=", id) + .execute(); + return row[0]; + }, + update: async (id, data) => { + await dbClient + .updateTable("users") + .set(data) + .where("id", "=", id) + .execute(); + }, + delete: async (id) => { + await dbClient.deleteFrom("users").where("id", "=", id).execute(); + }, + findByEmailAddress: async (emailAddress) => { + const row = await dbClient + .selectFrom("users") + .selectAll() + .where("email", "=", emailAddress) + .executeTakeFirst(); + return row; + }, + }; + const db = { conversations, facts, factTriggers, messages, + users, }; return db; diff --git a/layouts/LayoutDefault.tsx b/layouts/LayoutDefault.tsx index 350e33d..0f71d1a 100644 --- a/layouts/LayoutDefault.tsx +++ b/layouts/LayoutDefault.tsx @@ -4,6 +4,7 @@ import { AppShell, Box, Burger, + Button, Group, Image, MantineProvider, @@ -20,11 +21,12 @@ import { IconCircleFilled, IconTrashFilled, IconPlus, + IconBrandGoogle, } from "@tabler/icons-react"; import { useDisclosure } from "@mantine/hooks"; import theme from "./theme.js"; import logoUrl from "../assets/logo.png"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { TRPCProvider, useTRPC } from "../trpc/client.js"; import { usePageContext } from "vike-react/usePageContext"; import "./hover.css"; @@ -69,6 +71,71 @@ function getQueryClient() { return browserQueryClient; } +export function SignInWithGoogle() { + const pageContext = usePageContext(); + /** This is populated using the +onCreatePageContext.server.ts hook */ + const user = pageContext?.user; + const [csrfToken, setCsrfToken] = useState(""); + useEffect(() => { + fetch("/api/auth/csrf") + .then((res) => res.json()) + .then((obj) => setCsrfToken(obj.csrfToken)); + }, []); + if (user?.id) { + return ( +
+ + + Signed in as {user?.email} + +
+ ); + } + return ( +
+ + + +
+ ); +} + +export function SignOutButton() { + const handleSignOut = () => { + window.location.href = "/api/auth/signout"; + }; + + return ( + + ); +} + export default function LayoutDefault({ children, }: { @@ -133,6 +200,7 @@ export default function LayoutDefault({ > Token-Efficient Context Engineering + diff --git a/pages/+onCreatePageContext.server.ts b/pages/+onCreatePageContext.server.ts new file mode 100644 index 0000000..7451a9c --- /dev/null +++ b/pages/+onCreatePageContext.server.ts @@ -0,0 +1,22 @@ +import type { PageContextServer } from "vike/types"; + +// This hook is called upon new incoming HTTP requests +export async function onCreatePageContext(pageContext: PageContextServer) { + // // Select the properties you want to make available everywhere + pageContext.user = { + id: pageContext.session?.user?.id, + email: pageContext.session?.user?.email, + }; +} + +declare global { + namespace Vike { + // We extend PageContext instead of PageContextServer because user is passed to the client + interface PageContext { + user?: { + id: string | null | undefined; + email: string | null | undefined; + }; + } + } +} diff --git a/server/authjs-handler.ts b/server/authjs-handler.ts index 87e9df8..9399430 100644 --- a/server/authjs-handler.ts +++ b/server/authjs-handler.ts @@ -5,6 +5,7 @@ import { setEnvDefaults, } from "@auth/core"; import CredentialsProvider from "@auth/core/providers/credentials"; +import GoogleProvider from "@auth/core/providers/google"; import type { Session } from "@auth/core/types"; // TODO: stop using universal-middleware and directly integrate server middlewares instead and/or use vike-server https://vike.dev/server. (Bati generates boilerplates that use universal-middleware https://github.com/magne4000/universal-middleware to make Bati's internal logic easier. This is temporary and will be removed soon.) import type { @@ -12,17 +13,11 @@ import type { UniversalHandler, UniversalMiddleware, } from "@universal-middleware/core"; +import { env } from "./env.js"; +import { getDb } from "../database/index.js"; -const env: Record = - typeof process?.env !== "undefined" - ? process.env - : import.meta && "env" in import.meta - ? ( - import.meta as ImportMeta & { - env: Record; - } - ).env - : {}; +const POSTGRES_CONNECTION_STRING = + "postgres://neondb_owner:npg_sOVmj8vWq2zG@ep-withered-king-adiz9gpi-pooler.c-2.us-east-1.aws.neon.tech:5432/neondb?sslmode=require&channel_binding=true"; if (!globalThis.crypto) { /** @@ -43,7 +38,7 @@ const authjsConfig = { env.AUTH_TRUST_HOST ?? env.VERCEL ?? env.NODE_ENV !== "production" ), // TODO: Replace secret {@see https://authjs.dev/reference/core#secret} - secret: "MY_SECRET", + secret: "buginoo", providers: [ // TODO: Choose and implement providers CredentialsProvider({ @@ -66,7 +61,52 @@ const authjsConfig = { return user ?? null; }, }), + GoogleProvider({ + clientId: + "697711350664-t6237s5n3ttjd1npp1qif1aupptkr0va.apps.googleusercontent.com", + clientSecret: "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", + }), ], + callbacks: { + async signIn({ user, account, profile }) { + if (typeof user?.email !== "string") return false; + const db = await getDb(POSTGRES_CONNECTION_STRING); + const userFromDb = await db.users.findByEmailAddress(user.email); + if (!userFromDb) { + return false; + } + console.log("signIn", user, account, profile); + return true; + }, + jwt: async ({ token }) => { + if (typeof token?.email !== "string") return token; + const db = await getDb(POSTGRES_CONNECTION_STRING); + let userFromDb = await db.users.findByEmailAddress(token.email || ""); + if (!userFromDb) { + userFromDb = await db.users.create({ + // id: token.id, + email: token.email, + username: token.email, + password: null, + createdAt: null, + lastLogin: null, + }); + } + return { + ...token, + id: userFromDb?.id || "", + }; + }, + session: ({ token, session }) => { + return { + ...session, + user: { + ...session.user, + id: token.id as string, + }, + }; + }, + }, } satisfies Omit; /**