can sign in and out
This commit is contained in:
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
-11
@@ -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">;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user