can sign in and out

master
Avraham Sakal 4 weeks ago
parent 6d80d3a9a6
commit 83e3f867dd

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 B

@ -1,4 +1,5 @@
import type { CommittedMessage } from "../types"; import type { CommittedMessage } from "../types";
import type { Users } from "./generated/public/Users";
export type Conversation = { export type Conversation = {
id: string; id: string;
@ -25,6 +26,10 @@ export type FactTrigger = {
createdAt?: string; createdAt?: string;
}; };
export type User = Omit<Users, "id"> & {
id: string;
};
export interface Entity<T> { export interface Entity<T> {
construct: (data: T) => T; construct: (data: T) => T;
create: (data: Omit<T, "id">) => Promise<T>; create: (data: Omit<T, "id">) => Promise<T>;
@ -54,9 +59,14 @@ export type FactTriggerEntity = Entity<FactTrigger> & {
findByConversationId: (conversationId: string) => Promise<Array<FactTrigger>>; findByConversationId: (conversationId: string) => Promise<Array<FactTrigger>>;
}; };
export type UserEntity = Entity<User> & {
findByEmailAddress: (emailAddress: string) => Promise<User | undefined>;
};
export interface ApplicationDatabase { export interface ApplicationDatabase {
conversations: ConversationEntity; conversations: ConversationEntity;
factTriggers: FactTriggerEntity; factTriggers: FactTriggerEntity;
facts: FactEntity; facts: FactEntity;
messages: MessageEntity; messages: MessageEntity;
users: UserEntity;
} }

@ -8,6 +8,7 @@ import type {
FactEntity, FactEntity,
FactTriggerEntity, FactTriggerEntity,
MessageEntity, MessageEntity,
UserEntity,
} from "./common.ts"; } from "./common.ts";
import type { CommittedMessage } from "../types"; 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 = { const db = {
conversations, conversations,
facts, facts,
factTriggers, factTriggers,
messages, messages,
users,
}; };
return db; return db;

@ -4,6 +4,7 @@ import {
AppShell, AppShell,
Box, Box,
Burger, Burger,
Button,
Group, Group,
Image, Image,
MantineProvider, MantineProvider,
@ -20,11 +21,12 @@ import {
IconCircleFilled, IconCircleFilled,
IconTrashFilled, IconTrashFilled,
IconPlus, IconPlus,
IconBrandGoogle,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { useDisclosure } from "@mantine/hooks"; import { useDisclosure } from "@mantine/hooks";
import theme from "./theme.js"; import theme from "./theme.js";
import logoUrl from "../assets/logo.png"; import logoUrl from "../assets/logo.png";
import { useState } from "react"; import { useEffect, useState } from "react";
import { TRPCProvider, useTRPC } from "../trpc/client.js"; import { TRPCProvider, useTRPC } from "../trpc/client.js";
import { usePageContext } from "vike-react/usePageContext"; import { usePageContext } from "vike-react/usePageContext";
import "./hover.css"; import "./hover.css";
@ -69,6 +71,71 @@ function getQueryClient() {
return browserQueryClient; 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 (
<form
action="/api/auth/signout"
method="post"
className="flex items-center gap-2 px-4 py-2 border border-gray-300 rounded-md shadow-sm bg-white text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<input type="hidden" name="redirectTo" value={"/"} />
<input type="hidden" name="csrfToken" value={csrfToken} />
<span>Signed in as {user?.email}</span>
<Button
type="submit"
leftSection={<IconBrandGoogle />}
hidden={typeof user?.id === "string"}
>
Sign Out
</Button>
</form>
);
}
return (
<form
action="/api/auth/signin/google"
method="post"
className="flex items-center gap-2 px-4 py-2 border border-gray-300 rounded-md shadow-sm bg-white text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<input type="hidden" name="redirectTo" value={"/"} />
<input type="hidden" name="csrfToken" value={csrfToken} />
<Button
type="submit"
leftSection={<IconBrandGoogle />}
hidden={typeof user?.id === "string"}
>
Sign in with Google
</Button>
</form>
);
}
export function SignOutButton() {
const handleSignOut = () => {
window.location.href = "/api/auth/signout";
};
return (
<button
type="button"
onClick={handleSignOut}
className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
Sign Out
</button>
);
}
export default function LayoutDefault({ export default function LayoutDefault({
children, children,
}: { }: {
@ -133,6 +200,7 @@ export default function LayoutDefault({
> >
Token-Efficient Context Engineering Token-Efficient Context Engineering
</Title> </Title>
<SignInWithGoogle />
</Group> </Group>
</AppShell.Header> </AppShell.Header>
<AppShell.Navbar p="md"> <AppShell.Navbar p="md">

@ -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;
};
}
}
}

@ -5,6 +5,7 @@ import {
setEnvDefaults, setEnvDefaults,
} from "@auth/core"; } from "@auth/core";
import CredentialsProvider from "@auth/core/providers/credentials"; import CredentialsProvider from "@auth/core/providers/credentials";
import GoogleProvider from "@auth/core/providers/google";
import type { Session } from "@auth/core/types"; 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.) // 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 { import type {
@ -12,17 +13,11 @@ import type {
UniversalHandler, UniversalHandler,
UniversalMiddleware, UniversalMiddleware,
} from "@universal-middleware/core"; } from "@universal-middleware/core";
import { env } from "./env.js";
import { getDb } from "../database/index.js";
const env: Record<string, string | undefined> = const POSTGRES_CONNECTION_STRING =
typeof process?.env !== "undefined" "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";
? process.env
: import.meta && "env" in import.meta
? (
import.meta as ImportMeta & {
env: Record<string, string | undefined>;
}
).env
: {};
if (!globalThis.crypto) { if (!globalThis.crypto) {
/** /**
@ -43,7 +38,7 @@ const authjsConfig = {
env.AUTH_TRUST_HOST ?? env.VERCEL ?? env.NODE_ENV !== "production" env.AUTH_TRUST_HOST ?? env.VERCEL ?? env.NODE_ENV !== "production"
), ),
// TODO: Replace secret {@see https://authjs.dev/reference/core#secret} // TODO: Replace secret {@see https://authjs.dev/reference/core#secret}
secret: "MY_SECRET", secret: "buginoo",
providers: [ providers: [
// TODO: Choose and implement providers // TODO: Choose and implement providers
CredentialsProvider({ CredentialsProvider({
@ -66,7 +61,52 @@ const authjsConfig = {
return user ?? null; 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<AuthConfig, "raw">; } satisfies Omit<AuthConfig, "raw">;
/** /**

Loading…
Cancel
Save