Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
davidgomes committed Oct 12, 2024
0 parents commit a10f2a4
Show file tree
Hide file tree
Showing 33 changed files with 8,979 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
NEXT_PUBLIC_STACK_PROJECT_ID=
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=
STACK_SECRET_SERVER_KEY=

# For the `neondb_owner` role.
DATABASE_URL=
# For the `authenticated`, passwordless role.
DATABASE_AUTHENTICATED_URL=
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next"
}
37 changes: 37 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

.env
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Neon Authorize + Stack Auth Example (SQL from the Backend)

This repository is a guided getting started example for Neon Authorize + Stack Auth.

1. Create a Neon project
2. Sign Up for [Stack Auth](https://stack-auth.com/) and create a new project
3. Once in the Stack Auth's Dashboard, create a new project.
4. Head to the Neon Console, and find "Authorize"
5. Inside Authorize, click "Add Authentication Provider", choose Stack Auth and paste in the following URL (replacing your Stack Auth's Project ID):

```
https://api.stack-auth.com/api/v1/projects/<project-id>/.well-known/jwks.json
```

(If you have an older Stack Auth project, you'll have to disable legacy JWKS in the Stack Auth's project settings)

6. Clone this repository and run `npm install` or `bun install`
7. Create a `.env` file in the root of this project and add the following:

```
NEXT_PUBLIC_STACK_PROJECT_ID=
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=
STACK_SECRET_SERVER_KEY=
# For the `neondb_owner` role.
DATABASE_URL=
# For the `authenticated`, passwordless role.
DATABASE_AUTHENTICATED_URL=
```

8. Run `npm run drizzle:migrate` or `bun run drizzle:migrate` to apply the migrations
9. Run `npm run dev` or `bun run dev`
10. Open your browser and go to `http://localhost:3000`
11. Login and play around!
79 changes: 79 additions & 0 deletions app/actions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"use server";

import { fetchWithDrizzle } from "@/app/db";
import * as schema from "@/app/schema";
import { Todo } from "@/app/schema";
import { asc, eq, sql } from "drizzle-orm";
import { revalidatePath } from "next/cache";

export async function insertTodo(newTodo: { newTodo: string; userId: string }) {
await fetchWithDrizzle(async (db) => {
return db.insert(schema.todos).values({
task: newTodo.newTodo,
isComplete: false,
});
});

revalidatePath("/");
}

export async function getTodos(): Promise<Array<Todo>> {
return fetchWithDrizzle(async (db) => {
// WHERE filter is optional because of RLS. But we send it anyway for
// performance reasons.
return db
.select()
.from(schema.todos)
.where(eq(schema.todos.userId, sql`auth.user_id()`))
.orderBy(asc(schema.todos.insertedAt));
});
}

export async function deleteTodoFormAction(formData: FormData) {
const id = formData.get("id");
if (!id) {
throw new Error("No id");
}
if (typeof id !== "string") {
throw new Error("The id must be a string");
}

await fetchWithDrizzle(async (db) => {
return db.delete(schema.todos).where(eq(schema.todos.id, BigInt(id)));
});

revalidatePath("/");
}

export async function checkOrUncheckTodoFormAction(formData: FormData) {
const id = formData.get("id");
const isComplete = formData.get("isComplete");

if (!id) {
throw new Error("No id");
}

if (!isComplete) {
throw new Error("No isComplete");
}

if (typeof id !== "string") {
throw new Error("The id must be a string");
}

if (typeof isComplete !== "string") {
throw new Error("The isComplete must be a string");
}

const isCompleteBool = isComplete === "true";

await fetchWithDrizzle(async (db) => {
return db
.update(schema.todos)
.set({ isComplete: !isCompleteBool })
.where(eq(schema.todos.id, BigInt(id)))
.returning();
});

revalidatePath("/");
}
69 changes: 69 additions & 0 deletions app/add-todo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"use client";

import { insertTodo } from "@/app/actions";
import { CSSProperties, useRef } from "react";
import { useUser } from "@stackframe/stack";

const styles = {
form: {
display: "flex",
marginBottom: "20px",
gap: "10px",
},
input: {
flex: 1,
padding: "10px",
fontSize: "16px",
border: "1px solid #e0e0e0",
borderRadius: "4px",
outline: "none",
},
button: {
padding: "10px 20px",
fontSize: "16px",
backgroundColor: "#4CAF50",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
transition: "background-color 0.2s ease",
},
} satisfies Record<string, CSSProperties>;

export function AddTodoForm() {
const formRef = useRef<HTMLFormElement>(null);
const user = useUser();

const onSubmit = async (formData: FormData) => {
const newTodo = formData.get("newTodo");

if (!newTodo) {
throw new Error("No newTodo");
}

if (typeof newTodo !== "string") {
throw new Error("The newTodo must be a string");
}

if (!user) {
throw new Error("No userId");
}

await insertTodo({ newTodo: newTodo.toString(), userId: user.id });
formRef.current?.reset();
};

return (
<form ref={formRef} action={onSubmit} style={styles.form}>
<input
required
name="newTodo"
placeholder="Enter a new todo"
style={styles.input}
/>
<button type="submit" style={styles.button}>
Add Todo
</button>
</form>
);
}
37 changes: 37 additions & 0 deletions app/db.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as schema from "@/app/schema";
import { neon } from "@neondatabase/serverless";
import { drizzle, NeonHttpDatabase } from "drizzle-orm/neon-http";
import { stackServerApp } from "@/stack";

export async function fetchWithDrizzle<T>(
callback: (
db: NeonHttpDatabase<typeof schema>,
{ userId, authToken }: { userId: string; authToken: string },
) => Promise<T>,
) {
const user = await stackServerApp.getUser();
const authToken = (await user?.getAuthJson())?.accessToken;
if (!authToken) {
throw new Error("No token");
}

if (!user || !user.id) {
throw new Error("No userId");
}

const db = drizzle(
neon(process.env.DATABASE_AUTHENTICATED_URL!, {
authToken: async () => {
const token = (await user?.getAuthJson())?.accessToken;
console.log("authToken", token);
if (!token) {
throw new Error("No token");
}
return token;
},
}),
{ schema },
);

return callback(db, { userId: user.id, authToken });
}
Binary file added app/favicon.ico
Binary file not shown.
6 changes: 6 additions & 0 deletions app/handler/[...stack]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { StackHandler } from "@stackframe/stack";
import { stackServerApp } from "@/stack";

export default function Handler(props: Object) {
return <StackHandler fullPage app={stackServerApp} {...props} />;
}
27 changes: 27 additions & 0 deletions app/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use client";

import Link from "next/link";
import styles from "../styles/Home.module.css";
import { useStackApp, useUser } from "@stackframe/stack";

export function Header() {
const user = useUser();
const app = useStackApp();

return (
<header className={styles.header}>
<div>My Todo App</div>
{user ? (
<>
Hello {user.primaryEmail}
<Link href={app.urls.signOut}>Sign Out</Link>
</>
) : (
<span>
<Link href={app.urls.signIn}>Sign In</Link> |{" "}
<Link href={app.urls.signUp}>Sign Up</Link>
</span>
)}
</header>
);
}
19 changes: 19 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { StackProvider, StackTheme } from "@stackframe/stack";
import { stackServerApp } from "../stack";
import "../styles/globals.css";

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body className={`min-h-screen flex flex-col antialiased`}>
<StackProvider app={stackServerApp}>
<StackTheme>{children}</StackTheme>
</StackProvider>
</body>
</html>
);
}
5 changes: 5 additions & 0 deletions app/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default function Loading() {
// Stack uses React Suspense, which will render this page while user data is being fetched.
// See: https://nextjs.org/docs/app/api-reference/file-conventions/loading
return <></>;
}
29 changes: 29 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { AddTodoForm } from "@/app/add-todo";
import { Header } from "@/app/header";
import { TodoList } from "@/app/todo-list";

import styles from "../styles/Home.module.css";
import { stackServerApp } from "@/stack";

export default async function Home() {
const user = await stackServerApp.getUser();

let content = null;
if (user) {
content = (
<main className={styles.main}>
<div className={styles.container}>
<AddTodoForm />
<TodoList />
</div>
</main>
);
}

return (
<>
<Header />
{content}
</>
);
}
Loading

0 comments on commit a10f2a4

Please sign in to comment.