diff --git a/assets/google-logo.webp b/assets/google-logo.webp new file mode 100644 index 0000000..b57d89b Binary files /dev/null and b/assets/google-logo.webp differ diff --git a/database/common.ts b/database/common.ts index 0f9992b..f4b28ce 100644 --- a/database/common.ts +++ b/database/common.ts @@ -1,4 +1,5 @@ import type { CommittedMessage } from "../types"; +import type { Users } from "./generated/public/Users"; export type Conversation = { id: string; @@ -25,6 +26,10 @@ export type FactTrigger = { createdAt?: string; }; +export type User = Omit & { + 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; /**