From 085123c6901d8a18fbed9cc1d174dcbe43f5a754 Mon Sep 17 00:00:00 2001 From: znycheporuk Date: Tue, 4 Mar 2025 19:30:01 +0200 Subject: [PATCH] add drizzle zero better auth example --- .../with-drizzle-zero-better-auth/.env.local | 7 + .../with-drizzle-zero-better-auth/README.md | 53 +++ .../app.config.ts | 9 + .../docker/docker-compose.yml | 31 ++ .../docker/seed.sql | 3 + .../drizzle.config.ts | 15 + .../drizzle/0000_mighty_cockroach.sql | 71 +++ .../drizzle/meta/0000_snapshot.json | 430 ++++++++++++++++++ .../drizzle/meta/_journal.json | 13 + .../package.json | 42 ++ .../public/favicon.ico | Bin 0 -> 664 bytes .../with-drizzle-zero-better-auth/src/app.css | 11 + .../with-drizzle-zero-better-auth/src/app.tsx | 25 + .../src/components/header.tsx | 59 +++ .../src/components/zero-context.tsx | 47 ++ .../src/db/auth-schema.ts | 60 +++ .../src/db/index.ts | 19 + .../src/db/schema.ts | 15 + .../src/entry-client.tsx | 4 + .../src/entry-server.tsx | 21 + .../src/global.d.ts | 1 + .../src/lib/auth-client.ts | 6 + .../src/lib/auth.ts | 20 + .../src/lib/use-auth-redirect.ts | 19 + .../src/lib/use-cached-session.ts | 35 ++ .../src/lib/zero-schema.ts | 77 ++++ .../src/routes/(todos).tsx | 152 +++++++ .../src/routes/[...404].tsx | 30 ++ .../src/routes/api/*auth.ts | 4 + .../src/routes/login.tsx | 47 ++ .../src/routes/register.tsx | 54 +++ .../tsconfig.json | 19 + 32 files changed, 1399 insertions(+) create mode 100644 examples/with-drizzle-zero-better-auth/.env.local create mode 100644 examples/with-drizzle-zero-better-auth/README.md create mode 100644 examples/with-drizzle-zero-better-auth/app.config.ts create mode 100644 examples/with-drizzle-zero-better-auth/docker/docker-compose.yml create mode 100644 examples/with-drizzle-zero-better-auth/docker/seed.sql create mode 100644 examples/with-drizzle-zero-better-auth/drizzle.config.ts create mode 100644 examples/with-drizzle-zero-better-auth/drizzle/0000_mighty_cockroach.sql create mode 100644 examples/with-drizzle-zero-better-auth/drizzle/meta/0000_snapshot.json create mode 100644 examples/with-drizzle-zero-better-auth/drizzle/meta/_journal.json create mode 100644 examples/with-drizzle-zero-better-auth/package.json create mode 100644 examples/with-drizzle-zero-better-auth/public/favicon.ico create mode 100644 examples/with-drizzle-zero-better-auth/src/app.css create mode 100644 examples/with-drizzle-zero-better-auth/src/app.tsx create mode 100644 examples/with-drizzle-zero-better-auth/src/components/header.tsx create mode 100644 examples/with-drizzle-zero-better-auth/src/components/zero-context.tsx create mode 100644 examples/with-drizzle-zero-better-auth/src/db/auth-schema.ts create mode 100644 examples/with-drizzle-zero-better-auth/src/db/index.ts create mode 100644 examples/with-drizzle-zero-better-auth/src/db/schema.ts create mode 100644 examples/with-drizzle-zero-better-auth/src/entry-client.tsx create mode 100644 examples/with-drizzle-zero-better-auth/src/entry-server.tsx create mode 100644 examples/with-drizzle-zero-better-auth/src/global.d.ts create mode 100644 examples/with-drizzle-zero-better-auth/src/lib/auth-client.ts create mode 100644 examples/with-drizzle-zero-better-auth/src/lib/auth.ts create mode 100644 examples/with-drizzle-zero-better-auth/src/lib/use-auth-redirect.ts create mode 100644 examples/with-drizzle-zero-better-auth/src/lib/use-cached-session.ts create mode 100644 examples/with-drizzle-zero-better-auth/src/lib/zero-schema.ts create mode 100644 examples/with-drizzle-zero-better-auth/src/routes/(todos).tsx create mode 100644 examples/with-drizzle-zero-better-auth/src/routes/[...404].tsx create mode 100644 examples/with-drizzle-zero-better-auth/src/routes/api/*auth.ts create mode 100644 examples/with-drizzle-zero-better-auth/src/routes/login.tsx create mode 100644 examples/with-drizzle-zero-better-auth/src/routes/register.tsx create mode 100644 examples/with-drizzle-zero-better-auth/tsconfig.json diff --git a/examples/with-drizzle-zero-better-auth/.env.local b/examples/with-drizzle-zero-better-auth/.env.local new file mode 100644 index 000000000..3dc73bfe5 --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/.env.local @@ -0,0 +1,7 @@ +ZERO_UPSTREAM_DB="postgresql://user:password@127.0.0.1/zstart_solid" +ZERO_CVR_DB="postgresql://user:password@127.0.0.1/zstart_solid_cvr" +ZERO_CHANGE_DB="postgresql://user:password@127.0.0.1/zstart_solid_cdb" +ZERO_REPLICA_FILE="/tmp/zstart_solid_replica.db" +ZERO_AUTH_JWKS_URL="http://localhost:3000/api/auth/jwks" +BETTER_AUTH_SECRET="secretkey" +VITE_PUBLIC_SERVER='http://localhost:4848' diff --git a/examples/with-drizzle-zero-better-auth/README.md b/examples/with-drizzle-zero-better-auth/README.md new file mode 100644 index 000000000..3dbc04c75 --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/README.md @@ -0,0 +1,53 @@ +# SolidStart + +Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com); + +## Creating a project + +```bash +# create a new project in the current directory +npm init solid@latest + +# create a new project in my-app +npm init solid@latest my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```bash +# start postgres in docker container +npm run dev:db-up + +# start cache server +npm run dev:zero-cache + +# start the development server +npm run dev:ui +``` + +Once you change the drizzle schema, you need to update the migration files: + +```bash +# update the m +npx drizzle-kit generate +``` + +## Building + +Solid apps are built with _presets_, which optimise your project for deployment to different environments. + +By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`. + +## Documentation links: + +[SolidJS Documentation](https://docs.solidjs.com/) + +[Better Auth Documentation](https://www.better-auth.com/docs/introduction) + +[Zero Documentation](https://zero.rocicorp.dev/docs/introduction) + +[Drizzle ORM Documentation](https://orm.drizzle.team/docs/get-started) + +## This project was created with the [Solid CLI](https://solid-cli.netlify.app) diff --git a/examples/with-drizzle-zero-better-auth/app.config.ts b/examples/with-drizzle-zero-better-auth/app.config.ts new file mode 100644 index 000000000..bb80f55c6 --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/app.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "@solidjs/start/config"; +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + ssr: false, + vite: { + plugins: [tailwindcss()] + } +}); diff --git a/examples/with-drizzle-zero-better-auth/docker/docker-compose.yml b/examples/with-drizzle-zero-better-auth/docker/docker-compose.yml new file mode 100644 index 000000000..36ec0fcb4 --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/docker/docker-compose.yml @@ -0,0 +1,31 @@ +services: + postgres: + container_name: postgres + image: postgres:16.2-alpine + shm_size: 1g + user: postgres + restart: always + healthcheck: + test: "pg_isready -U user --dbname=postgres" + interval: 10s + timeout: 5s + retries: 5 + ports: + - 5432:5432 + environment: + POSTGRES_USER: user + POSTGRES_DB: postgres + POSTGRES_PASSWORD: password + command: | + postgres + -c wal_level=logical + -c max_wal_senders=10 + -c max_replication_slots=5 + -c hot_standby=on + -c hot_standby_feedback=on + volumes: + - ./.data/pgdata:/var/lib/postgresql/data + - ./:/docker-entrypoint-initdb.d +volumes: + docker_pgdata: + driver: local diff --git a/examples/with-drizzle-zero-better-auth/docker/seed.sql b/examples/with-drizzle-zero-better-auth/docker/seed.sql new file mode 100644 index 000000000..846db4587 --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/docker/seed.sql @@ -0,0 +1,3 @@ +CREATE DATABASE zstart_solid; +CREATE DATABASE zstart_solid_cvr; +CREATE DATABASE zstart_solid_cdb; diff --git a/examples/with-drizzle-zero-better-auth/drizzle.config.ts b/examples/with-drizzle-zero-better-auth/drizzle.config.ts new file mode 100644 index 000000000..7df2cbab9 --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/drizzle.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + dialect: "postgresql", + schema: ["./src/db/schema.ts", "./src/db/auth-schema.ts"], + casing: "snake_case", + dbCredentials: { + ssl: false, + user: "user", + password: "password", + host: "127.0.0.1", + port: 5432, + database: "zstart_solid" + } +}); diff --git a/examples/with-drizzle-zero-better-auth/drizzle/0000_mighty_cockroach.sql b/examples/with-drizzle-zero-better-auth/drizzle/0000_mighty_cockroach.sql new file mode 100644 index 000000000..48f1d58b7 --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/drizzle/0000_mighty_cockroach.sql @@ -0,0 +1,71 @@ +CREATE TYPE "public"."status" AS ENUM('active', 'done');--> statement-breakpoint +CREATE TABLE "todos" ( + "id" uuid PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "title" varchar(255) NOT NULL, + "status" "status" DEFAULT 'active' NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "accounts" ( + "id" text PRIMARY KEY NOT NULL, + "account_id" text NOT NULL, + "provider_id" text NOT NULL, + "user_id" text NOT NULL, + "access_token" text, + "refresh_token" text, + "id_token" text, + "access_token_expires_at" timestamp, + "refresh_token_expires_at" timestamp, + "scope" text, + "password" text, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE "jwkss" ( + "id" text PRIMARY KEY NOT NULL, + "public_key" text NOT NULL, + "private_key" text NOT NULL, + "created_at" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE "sessions" ( + "id" text PRIMARY KEY NOT NULL, + "expires_at" timestamp NOT NULL, + "token" text NOT NULL, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL, + "ip_address" text, + "user_agent" text, + "user_id" text NOT NULL, + CONSTRAINT "sessions_token_unique" UNIQUE("token") +); +--> statement-breakpoint +CREATE TABLE "users" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "email" text NOT NULL, + "email_verified" boolean NOT NULL, + "image" text, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL, + "username" text, + "display_username" text, + CONSTRAINT "users_email_unique" UNIQUE("email"), + CONSTRAINT "users_username_unique" UNIQUE("username") +); +--> statement-breakpoint +CREATE TABLE "verifications" ( + "id" text PRIMARY KEY NOT NULL, + "identifier" text NOT NULL, + "value" text NOT NULL, + "expires_at" timestamp NOT NULL, + "created_at" timestamp, + "updated_at" timestamp +); +--> statement-breakpoint +ALTER TABLE "todos" ADD CONSTRAINT "todos_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/examples/with-drizzle-zero-better-auth/drizzle/meta/0000_snapshot.json b/examples/with-drizzle-zero-better-auth/drizzle/meta/0000_snapshot.json new file mode 100644 index 000000000..5fd4801ae --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/drizzle/meta/0000_snapshot.json @@ -0,0 +1,430 @@ +{ + "id": "f51a27d1-ed16-40b9-a344-529e4f9d3f17", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.todos": { + "name": "todos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "todos_user_id_users_id_fk": { + "name": "todos_user_id_users_id_fk", + "tableFrom": "todos", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jwkss": { + "name": "jwkss", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_username": { + "name": "display_username", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": ["username"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verifications": { + "name": "verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.status": { + "name": "status", + "schema": "public", + "values": ["active", "done"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/examples/with-drizzle-zero-better-auth/drizzle/meta/_journal.json b/examples/with-drizzle-zero-better-auth/drizzle/meta/_journal.json new file mode 100644 index 000000000..cc114a999 --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1741044837332, + "tag": "0000_mighty_cockroach", + "breakpoints": true + } + ] +} diff --git a/examples/with-drizzle-zero-better-auth/package.json b/examples/with-drizzle-zero-better-auth/package.json new file mode 100644 index 000000000..8f3da7b1d --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/package.json @@ -0,0 +1,42 @@ +{ + "name": "example-with-better-auth-drizzle-zero", + "type": "module", + "scripts": { + "dev:ui": "vinxi dev", + "dev:zero-cache": "zero-cache-dev -p ./src/lib/zero-schema.ts", + "dev:db-up": "docker compose -f ./docker/docker-compose.yml --env-file .env up", + "dev:db-down": "docker compose -f ./docker/docker-compose.yml --env-file .env down", + "build": "vinxi build", + "start": "vinxi start", + "generate:auth-schema": "npx @better-auth/cli generate --output ./src/db/auth-schema.ts", + "generate:migration": "npx drizzle-kit generate" + }, + "dependencies": { + "@rocicorp/zero": "^0.16.2025022800", + "@solid-primitives/storage": "^4.3.1", + "@solidjs/router": "^0.15.3", + "@solidjs/start": "^1.1.2", + "better-auth": "^1.2.2", + "drizzle-orm": "^0.40.0", + "drizzle-zero": "^0.5.1", + "pg": "^8.13.3", + "solid-js": "^1.9.5", + "vinxi": "^0.5.3" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.9", + "drizzle-kit": "^0.30.5", + "tailwindcss": "^4.0.9" + }, + "engines": { + "node": ">=22" + }, + "trustedDependencies": [ + "@rocicorp/zero-sqlite3" + ], + "pnpm": { + "onlyBuiltDependencies": [ + "@rocicorp/zero-sqlite3" + ] + } +} diff --git a/examples/with-drizzle-zero-better-auth/public/favicon.ico b/examples/with-drizzle-zero-better-auth/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..fb282da0719ef6ab4c1732df93be6216b0d85520 GIT binary patch literal 664 zcmV;J0%!e+P)m9ebk1R zejT~~6f_`?;`cEd!+`7(hw@%%2;?RN8gX-L?z6cM( zKoG@&w+0}f@Pfvwc+deid)qgE!L$ENKYjViZC_Zcr>L(`2oXUT8f0mRQ(6-=HN_Ai zeBBEz3WP+1Cw`m!49Wf!MnZzp5bH8VkR~BcJ1s-j90TAS2Yo4j!J|KodxYR%3Numw zA?gq6e`5@!W~F$_De3yt&uspo&2yLb$(NwcPPI-4LGc!}HdY%jfq@AFs8LiZ4k(p} zZ!c9o+qbWYs-Mg zgdyTALzJX&7QXHdI_DPTFL33;w}88{e6Zk)MX0kN{3DX9uz#O_L58&XRH$Nvvu;fO zf&)7@?C~$z1K<>j0ga$$MIg+5xN;eQ?1-CA=`^Y169@Ab6!vcaNP=hxfKN%@Ly^R* zK1iv*s1Yl6_dVyz8>ZqYhz6J4|3fQ@2LQeX@^%W(B~8>=MoEmBEGGD1;gHXlpX>!W ym)!leA2L@`cpb^hy)P75=I!`pBYxP7<2VfQ3j76qLgzIA0000 { + useAuthRedirect(); + return ( + +
+ {props.children} + + ); + }} + > + + + ); +} diff --git a/examples/with-drizzle-zero-better-auth/src/components/header.tsx b/examples/with-drizzle-zero-better-auth/src/components/header.tsx new file mode 100644 index 000000000..78da1e473 --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/src/components/header.tsx @@ -0,0 +1,59 @@ +import { useLocation } from "@solidjs/router"; +import { Show } from "solid-js"; +import { authClient } from "~/lib/auth-client"; +import { useCachedSession } from "~/lib/use-cached-session"; + +export default function Header() { + const location = useLocation(); + const active = (path: string) => + path === location.pathname ? "border-sky-600" : "border-transparent hover:border-sky-600"; + + return ( +
+ + + +
+ ); +} + +function AuthButtons() { + const session = useCachedSession(); + const location = useLocation(); + + const active = (path: string) => + path === location.pathname ? "border-sky-600" : "border-transparent hover:border-sky-600"; + return ( + + + + } + > + + + ); +} diff --git a/examples/with-drizzle-zero-better-auth/src/components/zero-context.tsx b/examples/with-drizzle-zero-better-auth/src/components/zero-context.tsx new file mode 100644 index 000000000..57e862f93 --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/src/components/zero-context.tsx @@ -0,0 +1,47 @@ +import type { Zero } from "@rocicorp/zero"; +import { createZero } from "@rocicorp/zero/solid"; +import { type Accessor, type ParentProps, createContext, createMemo, useContext } from "solid-js"; +import { useCachedSession } from "~/lib/use-cached-session"; +import { schema } from "~/lib/zero-schema"; + +const Context = createContext(); + +export function ZeroContext(props: ParentProps) { + const session = useCachedSession(); + + const z = createMemo(() => { + const jwtStorageKey = `jwt-${session.data?.user.id}`; + + console.log("createZero", session.data?.user.id); + return createZero({ + userID: session.data?.user.id ?? "null", + auth: async error => { + if (error === "invalid-token") { + sessionStorage.removeItem(jwtStorageKey); + } + + let token = sessionStorage.getItem(jwtStorageKey); + if (!token) { + if (!session.data?.user) return undefined; + const response = await fetch("/api/auth/token"); + const data = await response.json(); + token = data.token; + if (!token) throw new Error("No token found"); + sessionStorage.setItem(jwtStorageKey, token); + } + console.log("token", jwtStorageKey, token); + return token ?? undefined; + }, + server: import.meta.env.VITE_PUBLIC_SERVER, + schema, + kvStore: "mem" + }); + }); + + return {props.children}; +} + +export function useZero() { + const z = useContext(Context); + return z as Accessor>; +} diff --git a/examples/with-drizzle-zero-better-auth/src/db/auth-schema.ts b/examples/with-drizzle-zero-better-auth/src/db/auth-schema.ts new file mode 100644 index 000000000..3a93da58e --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/src/db/auth-schema.ts @@ -0,0 +1,60 @@ +import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core"; + +export const users = pgTable("users", { + id: text("id").primaryKey(), + name: text("name").notNull(), + email: text("email").notNull().unique(), + emailVerified: boolean("email_verified").notNull(), + image: text("image"), + createdAt: timestamp("created_at").notNull(), + updatedAt: timestamp("updated_at").notNull(), + username: text("username").unique(), + displayUsername: text("display_username") +}); + +export const sessions = pgTable("sessions", { + id: text("id").primaryKey(), + expiresAt: timestamp("expires_at").notNull(), + token: text("token").notNull().unique(), + createdAt: timestamp("created_at").notNull(), + updatedAt: timestamp("updated_at").notNull(), + ipAddress: text("ip_address"), + userAgent: text("user_agent"), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }) +}); + +export const accounts = pgTable("accounts", { + id: text("id").primaryKey(), + accountId: text("account_id").notNull(), + providerId: text("provider_id").notNull(), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + accessToken: text("access_token"), + refreshToken: text("refresh_token"), + idToken: text("id_token"), + accessTokenExpiresAt: timestamp("access_token_expires_at"), + refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), + scope: text("scope"), + password: text("password"), + createdAt: timestamp("created_at").notNull(), + updatedAt: timestamp("updated_at").notNull() +}); + +export const verifications = pgTable("verifications", { + id: text("id").primaryKey(), + identifier: text("identifier").notNull(), + value: text("value").notNull(), + expiresAt: timestamp("expires_at").notNull(), + createdAt: timestamp("created_at"), + updatedAt: timestamp("updated_at") +}); + +export const jwkss = pgTable("jwkss", { + id: text("id").primaryKey(), + publicKey: text("public_key").notNull(), + privateKey: text("private_key").notNull(), + createdAt: timestamp("created_at").notNull() +}); diff --git a/examples/with-drizzle-zero-better-auth/src/db/index.ts b/examples/with-drizzle-zero-better-auth/src/db/index.ts new file mode 100644 index 000000000..b863a898f --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/src/db/index.ts @@ -0,0 +1,19 @@ +import { drizzle } from "drizzle-orm/node-postgres"; +import { migrate } from "drizzle-orm/node-postgres/migrator"; +import * as authSchema from "./auth-schema"; +import * as schema from "./schema"; + +if (!process.env.ZERO_UPSTREAM_DB) { + throw new Error("ZERO_UPSTREAM_DB is not set"); +} + +export const db = drizzle(process.env.ZERO_UPSTREAM_DB, { + schema: { ...authSchema, ...schema }, + casing: "snake_case" +}); + +(async () => { + await migrate(db, { + migrationsFolder: "drizzle" + }); +})(); diff --git a/examples/with-drizzle-zero-better-auth/src/db/schema.ts b/examples/with-drizzle-zero-better-auth/src/db/schema.ts new file mode 100644 index 000000000..8e4a02bb9 --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/src/db/schema.ts @@ -0,0 +1,15 @@ +import { pgEnum, pgTable, text, timestamp, uuid, varchar } from "drizzle-orm/pg-core"; +import { users } from "./auth-schema"; + +export const statusEnum = pgEnum("status", ["active", "done"]); + +export const todos = pgTable("todos", { + id: uuid().primaryKey(), + userId: text() + .notNull() + .references(() => users.id), + title: varchar({ length: 255 }).notNull(), + status: statusEnum().notNull().default("active"), + createdAt: timestamp().defaultNow().notNull(), + updatedAt: timestamp().defaultNow().notNull() +}); diff --git a/examples/with-drizzle-zero-better-auth/src/entry-client.tsx b/examples/with-drizzle-zero-better-auth/src/entry-client.tsx new file mode 100644 index 000000000..0ca4e3c30 --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/src/entry-client.tsx @@ -0,0 +1,4 @@ +// @refresh reload +import { mount, StartClient } from "@solidjs/start/client"; + +mount(() => , document.getElementById("app")!); diff --git a/examples/with-drizzle-zero-better-auth/src/entry-server.tsx b/examples/with-drizzle-zero-better-auth/src/entry-server.tsx new file mode 100644 index 000000000..6d6e41a24 --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/src/entry-server.tsx @@ -0,0 +1,21 @@ +// @refresh reload +import { StartServer, createHandler } from "@solidjs/start/server"; + +export default createHandler(() => ( + ( + + + + + + {assets} + + +
{children}
+ {scripts} + + + )} + /> +)); diff --git a/examples/with-drizzle-zero-better-auth/src/global.d.ts b/examples/with-drizzle-zero-better-auth/src/global.d.ts new file mode 100644 index 000000000..dc6f10c22 --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/src/global.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/with-drizzle-zero-better-auth/src/lib/auth-client.ts b/examples/with-drizzle-zero-better-auth/src/lib/auth-client.ts new file mode 100644 index 000000000..e4fbf30a3 --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/src/lib/auth-client.ts @@ -0,0 +1,6 @@ +import { usernameClient } from "better-auth/client/plugins"; +import { createAuthClient } from "better-auth/solid"; + +export const authClient = createAuthClient({ + plugins: [usernameClient()] +}); diff --git a/examples/with-drizzle-zero-better-auth/src/lib/auth.ts b/examples/with-drizzle-zero-better-auth/src/lib/auth.ts new file mode 100644 index 000000000..b7044f3b6 --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/src/lib/auth.ts @@ -0,0 +1,20 @@ +import { betterAuth } from "better-auth"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { jwt, username } from "better-auth/plugins"; +import { db } from "../db"; + +export const auth = betterAuth({ + database: drizzleAdapter(db, { + provider: "pg", + usePlural: true + }), + emailAndPassword: { + enabled: true + }, + session: { + cookieCache: { + enabled: true + } + }, + plugins: [username(), jwt()] +}); diff --git a/examples/with-drizzle-zero-better-auth/src/lib/use-auth-redirect.ts b/examples/with-drizzle-zero-better-auth/src/lib/use-auth-redirect.ts new file mode 100644 index 000000000..8bb242535 --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/src/lib/use-auth-redirect.ts @@ -0,0 +1,19 @@ +import { useLocation, useNavigate } from "@solidjs/router"; +import { createEffect } from "solid-js"; +import { useCachedSession } from "./use-cached-session"; + +const authPages = ["/login", "/register"]; + +export function useAuthRedirect() { + const session = useCachedSession(); + const navigate = useNavigate(); + const location = useLocation(); + + createEffect(() => { + if (!session.isPending && session.data && authPages.includes(location.pathname)) { + navigate("/"); + } else if (!session.isPending && !session.data && !authPages.includes(location.pathname)) { + navigate("/login"); + } + }); +} diff --git a/examples/with-drizzle-zero-better-auth/src/lib/use-cached-session.ts b/examples/with-drizzle-zero-better-auth/src/lib/use-cached-session.ts new file mode 100644 index 000000000..7aac2c43b --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/src/lib/use-cached-session.ts @@ -0,0 +1,35 @@ +import { makePersisted } from "@solid-primitives/storage"; +import { createEffect } from "solid-js"; +import { createStore } from "solid-js/store"; +import { authClient } from "./auth-client"; + +// on page reload, isPending is true, and data is null, to prevent flicker, we can use a persisted store to store the data, and update it when the session is not pending + +export function useCachedSession() { + const session = authClient.useSession(); + const [store, setStore] = makePersisted(createStore({ data: session().data }), { + name: "session.data", + storage: sessionStorage + }); + + createEffect(() => { + if (!session().isPending) { + setStore("data", session().data); + } + }); + + return { + get isPending() { + return session().isPending; + }, + get data() { + return store.data; + }, + get error() { + return session().error; + }, + get isRefetching() { + return session().isRefetching; + } + }; +} diff --git a/examples/with-drizzle-zero-better-auth/src/lib/zero-schema.ts b/examples/with-drizzle-zero-better-auth/src/lib/zero-schema.ts new file mode 100644 index 000000000..1889369f5 --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/src/lib/zero-schema.ts @@ -0,0 +1,77 @@ +import { + ANYONE_CAN, + type ExpressionBuilder, + type PermissionsConfig, + type Row, + definePermissions +} from "@rocicorp/zero"; +import { createZeroSchema } from "drizzle-zero"; +import * as drizzleAuthSchema from "../db/auth-schema"; +import * as drizzleSchema from "../db/schema"; + +export const schema = createZeroSchema( + { ...drizzleAuthSchema, ...drizzleSchema }, + { + version: 1, + casing: "snake_case", + tables: { + verifications: false, + accounts: false, + sessions: false, + jwkss: false, + users: { + id: true, + email: true, + emailVerified: true, + username: true, + createdAt: true, + updatedAt: true, + displayUsername: true, + image: true, + name: true + }, + todos: { + id: true, + userId: true, + title: true, + status: true, + createdAt: true, + updatedAt: true + } + } + } +); + +type AuthData = { + sub: string; +}; + +export const permissions = definePermissions(schema, () => { + const allowIfTodoCreator = (authData: AuthData, { cmp }: ExpressionBuilder) => + cmp("userId", "=", authData.sub); + + return { + todos: { + row: { + select: [allowIfTodoCreator], + insert: ANYONE_CAN, + update: { + postMutation: [allowIfTodoCreator], + preMutation: [allowIfTodoCreator] + }, + delete: [allowIfTodoCreator] + } + }, + users: { + row: { + select: ANYONE_CAN + // Other operations are denied by default + // Other tables are denied by default. + } + } + } satisfies PermissionsConfig; +}); + +export type Schema = typeof schema; +type User = Row; +type Todo = Row; diff --git a/examples/with-drizzle-zero-better-auth/src/routes/(todos).tsx b/examples/with-drizzle-zero-better-auth/src/routes/(todos).tsx new file mode 100644 index 000000000..3bdd55c75 --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/src/routes/(todos).tsx @@ -0,0 +1,152 @@ +import { useQuery } from "@rocicorp/zero/solid"; +import { action } from "@solidjs/router"; +import { For, Show, createSignal } from "solid-js"; +import { useZero } from "~/components/zero-context"; +import { useCachedSession } from "~/lib/use-cached-session"; + +export default function Home() { + const session = useCachedSession(); + const userId = () => session.data?.user.id ?? "anon"; + const z = useZero(); + const [editingTodo, setEditingTodo] = createSignal(null); + let inputRef: HTMLInputElement | undefined; + + const [todosList] = useQuery(() => + z().query.todos.where("userId", "=", userId()).orderBy("createdAt", "desc") + ); + + const addTodo = action(async (formData: FormData) => { + const newTodo = formData.get("newTodo") as string; + if (!newTodo?.trim()) return; + await z().mutate.todos.insert({ + id: crypto.randomUUID(), + userId: userId(), + title: newTodo, + status: "active", + createdAt: Date.now(), + updatedAt: Date.now() + }); + if (inputRef) inputRef.value = ""; + }, "add-todo"); + + const editTodo = action(async (formData: FormData) => { + const id = formData.get("id") as string; + const title = formData.get("title") as string; + if (!title.trim()) return; + await z().mutate.todos.update({ + id, + title, + updatedAt: Date.now() + }); + setEditingTodo(null); + }); + + const updateTodoStatus = async (id: string, status: "active" | "done") => { + await z().mutate.todos.update({ + id, + status, + updatedAt: Date.now() + }); + }; + + const deleteTodo = async (id: string) => { + await z().mutate.todos.delete({ id }); + }; + + return ( +
+

Todos

+
+
+ + +
+ +
    + + {todo => ( +
  • + + + { + if (e.key === "Escape") { + setEditingTodo(null); + } + }} + class="grow rounded-lg border p-2 shadow-sm" + /> + + + + } + > + { + updateTodoStatus(todo.id, todo.status === "active" ? "done" : "active"); + }} + /> + + {todo.title} + +
    + + +
    +
    +
  • + )} +
    +
+
+
+ ); +} diff --git a/examples/with-drizzle-zero-better-auth/src/routes/[...404].tsx b/examples/with-drizzle-zero-better-auth/src/routes/[...404].tsx new file mode 100644 index 000000000..a9c14e974 --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/src/routes/[...404].tsx @@ -0,0 +1,30 @@ +import { A } from "@solidjs/router"; + +export default function NotFound() { + return ( +
+

Not Found

+

+ Visit{" "} + + solidjs.com + {" "} + to learn how to build Solid apps. +

+

+ + Home + + {" - "} + + About Page + +

+
+ ); +} diff --git a/examples/with-drizzle-zero-better-auth/src/routes/api/*auth.ts b/examples/with-drizzle-zero-better-auth/src/routes/api/*auth.ts new file mode 100644 index 000000000..59d394947 --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/src/routes/api/*auth.ts @@ -0,0 +1,4 @@ +import { toSolidStartHandler } from "better-auth/solid-start"; +import { auth } from "~/lib/auth"; + +export const { GET, POST } = toSolidStartHandler(auth); diff --git a/examples/with-drizzle-zero-better-auth/src/routes/login.tsx b/examples/with-drizzle-zero-better-auth/src/routes/login.tsx new file mode 100644 index 000000000..6ef661438 --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/src/routes/login.tsx @@ -0,0 +1,47 @@ +import { action, useSubmission } from "@solidjs/router"; +import { Show } from "solid-js"; +import { authClient } from "~/lib/auth-client"; + +const onLogin = action(async (formData: FormData) => { + const username = formData.get("username") as string; + const password = formData.get("password") as string; + const { error } = await authClient.signIn.username({ + username, + password + }); + if (error) { + throw error; + } + + return { success: true }; +}, "login"); + +export default function Login() { + const submission = useSubmission(onLogin); + + return ( +
+

Login

+
+ + + +
Error: {submission.error.message}
+
+ +
+
+ ); +} diff --git a/examples/with-drizzle-zero-better-auth/src/routes/register.tsx b/examples/with-drizzle-zero-better-auth/src/routes/register.tsx new file mode 100644 index 000000000..acbe5b24a --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/src/routes/register.tsx @@ -0,0 +1,54 @@ +import { action, useSubmission } from "@solidjs/router"; +import { Show } from "solid-js"; +import { authClient } from "~/lib/auth-client"; + +const onRegister = action(async (formData: FormData) => { + const username = formData.get("username") as string; + const email = formData.get("email") as string; + const password = formData.get("password") as string; + + const { error } = await authClient.signUp.email({ + email, + name: username, + password, + username + }); + if (error) { + throw error; + } + return { success: true }; +}, "register"); + +export default function Register() { + const submission = useSubmission(onRegister); + + return ( +
+

Register

+
+ + + + +
Error: {submission.error.message}
+
+ +
+
+ ); +} diff --git a/examples/with-drizzle-zero-better-auth/tsconfig.json b/examples/with-drizzle-zero-better-auth/tsconfig.json new file mode 100644 index 000000000..3ad477f77 --- /dev/null +++ b/examples/with-drizzle-zero-better-auth/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowJs": true, + "noEmit": true, + "strict": true, + "types": ["vinxi/types/client"], + "isolatedModules": true, + "paths": { + "~/*": ["./src/*"] + } + } +}