feat: setup better-auth
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kan/auth": "workspace:*",
|
||||
"@kan/db": "workspace:*",
|
||||
"@kan/email": "workspace:^",
|
||||
"@kan/shared": "workspace:^",
|
||||
|
||||
@@ -6,18 +6,22 @@ import superjson from "superjson";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import type { dbClient } from "@kan/db/client";
|
||||
import type { Database } from "@kan/db/types/database.types";
|
||||
import type { SupabaseClient } from "@kan/supabase";
|
||||
import { auth } from "@kan/auth";
|
||||
import { createDrizzleClient } from "@kan/db/client";
|
||||
import { createNextApiClient, createTRPCClient } from "@kan/supabase";
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
image?: string | null | undefined;
|
||||
stripeCustomerId?: string | null | undefined;
|
||||
}
|
||||
|
||||
interface CreateContextOptions {
|
||||
user: User | null;
|
||||
supabaseClient: SupabaseClient<Database>;
|
||||
user: User | null | undefined;
|
||||
db: dbClient;
|
||||
}
|
||||
|
||||
@@ -25,28 +29,22 @@ export const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
||||
return {
|
||||
user: opts.user,
|
||||
db: opts.db,
|
||||
supabaseClient: opts.supabaseClient,
|
||||
};
|
||||
};
|
||||
|
||||
export const createTRPCContext = async ({
|
||||
req,
|
||||
resHeaders,
|
||||
}: FetchCreateContextFnOptions) => {
|
||||
const supabaseClient = createTRPCClient(req, resHeaders);
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabaseClient.auth.getUser();
|
||||
const session = await auth.api.getSession({
|
||||
headers: req.headers,
|
||||
});
|
||||
|
||||
const db = createDrizzleClient();
|
||||
|
||||
return createInnerTRPCContext({ db, user, supabaseClient: supabaseClient });
|
||||
return createInnerTRPCContext({ db, user: session?.user });
|
||||
};
|
||||
|
||||
export const createRESTContext = async ({ req }: CreateNextContextOptions) => {
|
||||
const supabaseClient = createNextApiClient(req);
|
||||
|
||||
const authHeader = req.headers.authorization;
|
||||
const accessToken = authHeader?.startsWith("Bearer ")
|
||||
? authHeader.substring(7)
|
||||
@@ -55,14 +53,14 @@ export const createRESTContext = async ({ req }: CreateNextContextOptions) => {
|
||||
const db = createDrizzleClient();
|
||||
|
||||
if (!accessToken) {
|
||||
return createInnerTRPCContext({ db, user: null, supabaseClient });
|
||||
return createInnerTRPCContext({ db, user: null });
|
||||
}
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabaseClient.auth.getUser(accessToken);
|
||||
const session = await auth.api.getSession({
|
||||
headers: req.headers,
|
||||
});
|
||||
|
||||
return createInnerTRPCContext({ db, user, supabaseClient });
|
||||
return createInnerTRPCContext({ db, user: session?.user });
|
||||
};
|
||||
|
||||
const t = initTRPC
|
||||
|
||||
9
packages/auth/eslint.config.js
Normal file
9
packages/auth/eslint.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import baseConfig from "@kan/eslint-config/base";
|
||||
|
||||
/** @type {import('typescript-eslint').Config} */
|
||||
export default [
|
||||
{
|
||||
ignores: [],
|
||||
},
|
||||
...baseConfig,
|
||||
];
|
||||
32
packages/auth/package.json
Normal file
32
packages/auth/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@kan/auth",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"clean": "git clean -xdf .cache .turbo dist node_modules",
|
||||
"dev": "tsc",
|
||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||
"lint": "eslint",
|
||||
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kan/db": "workspace:*",
|
||||
"@kan/eslint-config": "workspace:*",
|
||||
"@kan/prettier-config": "workspace:*",
|
||||
"@kan/shared": "workspace:*",
|
||||
"@kan/tsconfig": "workspace:*",
|
||||
"eslint": "catalog:",
|
||||
"prettier": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"prettier": "@kan/prettier-config",
|
||||
"dependencies": {
|
||||
"better-auth": "^1.2.7"
|
||||
}
|
||||
}
|
||||
52
packages/auth/src/auth.ts
Normal file
52
packages/auth/src/auth.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { apiKey } from "better-auth/plugins";
|
||||
import { magicLink } from "better-auth/plugins/magic-link";
|
||||
|
||||
import { createDrizzleClient } from "@kan/db/client";
|
||||
import * as schema from "@kan/db/schema";
|
||||
|
||||
const db = createDrizzleClient();
|
||||
|
||||
console.log("GOOGLE_CLIENT_ID", process.env.GOOGLE_CLIENT_ID);
|
||||
console.log("GOOGLE_CLIENT_SECRET", process.env.GOOGLE_CLIENT_SECRET);
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "pg",
|
||||
schema: {
|
||||
...schema,
|
||||
user: schema.users,
|
||||
},
|
||||
}),
|
||||
socialProviders: {
|
||||
google: {
|
||||
clientId: process.env.GOOGLE_CLIENT_ID!,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
additionalFields: {
|
||||
stripeCustomerId: {
|
||||
type: "string",
|
||||
required: false,
|
||||
defaultValue: null,
|
||||
input: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
apiKey(),
|
||||
magicLink({
|
||||
sendMagicLink: async ({ email, token, url }, request) => {
|
||||
// send email to user
|
||||
},
|
||||
}),
|
||||
],
|
||||
advanced: {
|
||||
cookiePrefix: "kan",
|
||||
database: {
|
||||
generateId: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
3
packages/auth/src/clients.ts
Normal file
3
packages/auth/src/clients.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { createAuthClient } from "better-auth/react";
|
||||
|
||||
export const authClient = createAuthClient();
|
||||
6
packages/auth/src/index.ts
Normal file
6
packages/auth/src/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { auth } from "./auth";
|
||||
import { authClient } from "./clients";
|
||||
|
||||
export const name = "auth";
|
||||
|
||||
export { auth, authClient };
|
||||
6
packages/auth/tsconfig.json
Normal file
6
packages/auth/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "@kan/tsconfig/internal-package.json",
|
||||
"compilerOptions": {},
|
||||
"include": ["*.ts", "src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
87
packages/db/migrations/0010_wet_true_believers.sql
Normal file
87
packages/db/migrations/0010_wet_true_believers.sql
Normal file
@@ -0,0 +1,87 @@
|
||||
CREATE TABLE IF NOT EXISTS "account" (
|
||||
"id" bigserial PRIMARY KEY NOT NULL,
|
||||
"accountId" text NOT NULL,
|
||||
"providerId" text NOT NULL,
|
||||
"userId" uuid NOT NULL,
|
||||
"accessToken" text,
|
||||
"refreshToken" text,
|
||||
"idToken" text,
|
||||
"accessTokenExpiresAt" timestamp,
|
||||
"refreshTokenExpiresAt" timestamp,
|
||||
"scope" text,
|
||||
"password" text,
|
||||
"createdAt" timestamp NOT NULL,
|
||||
"updatedAt" timestamp NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "account" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "apikey" (
|
||||
"id" bigserial PRIMARY KEY NOT NULL,
|
||||
"name" text,
|
||||
"start" text,
|
||||
"prefix" text,
|
||||
"key" text NOT NULL,
|
||||
"userId" uuid NOT NULL,
|
||||
"refillInterval" integer,
|
||||
"refillAmount" integer,
|
||||
"lastRefillAt" timestamp,
|
||||
"enabled" boolean,
|
||||
"rateLimitEnabled" boolean,
|
||||
"rateLimitTimeWindow" integer,
|
||||
"rateLimitMax" integer,
|
||||
"requestCount" integer,
|
||||
"remaining" integer,
|
||||
"lastRequest" timestamp,
|
||||
"expiresAt" timestamp,
|
||||
"createdAt" timestamp NOT NULL,
|
||||
"updatedAt" timestamp NOT NULL,
|
||||
"permissions" text,
|
||||
"metadata" text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "apikey" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "session" (
|
||||
"id" bigserial PRIMARY KEY NOT NULL,
|
||||
"expiresAt" timestamp NOT NULL,
|
||||
"token" text NOT NULL,
|
||||
"createdAt" timestamp NOT NULL,
|
||||
"updatedAt" timestamp NOT NULL,
|
||||
"ipAddress" text,
|
||||
"userAgent" text,
|
||||
"userId" uuid NOT NULL,
|
||||
CONSTRAINT "session_token_unique" UNIQUE("token")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "session" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "verification" (
|
||||
"id" bigserial PRIMARY KEY NOT NULL,
|
||||
"identifier" text NOT NULL,
|
||||
"value" text NOT NULL,
|
||||
"expiresAt" timestamp NOT NULL,
|
||||
"createdAt" timestamp,
|
||||
"updatedAt" timestamp
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "verification" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "user" ALTER COLUMN "id" SET DEFAULT uuid_generate_v4();--> statement-breakpoint
|
||||
ALTER TABLE "user" ALTER COLUMN "emailVerified" SET DATA TYPE boolean;--> statement-breakpoint
|
||||
ALTER TABLE "user" ALTER COLUMN "emailVerified" SET NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "user" ADD COLUMN "createdAt" timestamp DEFAULT now() NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "user" ADD COLUMN "updatedAt" timestamp DEFAULT now() NOT NULL;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "account" ADD CONSTRAINT "account_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "apikey" ADD CONSTRAINT "apikey_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "session" ADD CONSTRAINT "session_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
8
packages/db/migrations/0011_cultured_red_skull.sql
Normal file
8
packages/db/migrations/0011_cultured_red_skull.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
ALTER TABLE "apikey" RENAME TO "apiKey";--> statement-breakpoint
|
||||
ALTER TABLE "apiKey" DROP CONSTRAINT "apikey_userId_user_id_fk";
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "apiKey" ADD CONSTRAINT "apiKey_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
2056
packages/db/migrations/meta/0010_snapshot.json
Normal file
2056
packages/db/migrations/meta/0010_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2056
packages/db/migrations/meta/0011_snapshot.json
Normal file
2056
packages/db/migrations/meta/0011_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -71,6 +71,20 @@
|
||||
"when": 1745407291997,
|
||||
"tag": "0009_shallow_silver_surfer",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 10,
|
||||
"version": "7",
|
||||
"when": 1746224485085,
|
||||
"tag": "0010_wet_true_believers",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 11,
|
||||
"version": "7",
|
||||
"when": 1746224588889,
|
||||
"tag": "0011_cultured_red_skull",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -37,6 +37,7 @@ export const create = async (
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
stripeCustomerId: user.stripeCustomerId,
|
||||
emailVerified: false,
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
||||
77
packages/db/src/schema/auth.ts
Normal file
77
packages/db/src/schema/auth.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import {
|
||||
bigserial,
|
||||
boolean,
|
||||
integer,
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
import { users } from "./users";
|
||||
|
||||
export const session = pgTable("session", {
|
||||
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||
expiresAt: timestamp("expiresAt").notNull(),
|
||||
token: text("token").notNull().unique(),
|
||||
createdAt: timestamp("createdAt").notNull(),
|
||||
updatedAt: timestamp("updatedAt").notNull(),
|
||||
ipAddress: text("ipAddress"),
|
||||
userAgent: text("userAgent"),
|
||||
userId: uuid("userId")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
}).enableRLS();
|
||||
|
||||
export const account = pgTable("account", {
|
||||
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||
accountId: text("accountId").notNull(),
|
||||
providerId: text("providerId").notNull(),
|
||||
userId: uuid("userId")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
accessToken: text("accessToken"),
|
||||
refreshToken: text("refreshToken"),
|
||||
idToken: text("idToken"),
|
||||
accessTokenExpiresAt: timestamp("accessTokenExpiresAt"),
|
||||
refreshTokenExpiresAt: timestamp("refreshTokenExpiresAt"),
|
||||
scope: text("scope"),
|
||||
password: text("password"),
|
||||
createdAt: timestamp("createdAt").notNull(),
|
||||
updatedAt: timestamp("updatedAt").notNull(),
|
||||
}).enableRLS();
|
||||
|
||||
export const verification = pgTable("verification", {
|
||||
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||
identifier: text("identifier").notNull(),
|
||||
value: text("value").notNull(),
|
||||
expiresAt: timestamp("expiresAt").notNull(),
|
||||
createdAt: timestamp("createdAt"),
|
||||
updatedAt: timestamp("updatedAt"),
|
||||
}).enableRLS();
|
||||
|
||||
export const apiKey = pgTable("apiKey", {
|
||||
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||
name: text("name"),
|
||||
start: text("start"),
|
||||
prefix: text("prefix"),
|
||||
key: text("key").notNull(),
|
||||
userId: uuid("userId")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
refillInterval: integer("refillInterval"),
|
||||
refillAmount: integer("refillAmount"),
|
||||
lastRefillAt: timestamp("lastRefillAt"),
|
||||
enabled: boolean("enabled"),
|
||||
rateLimitEnabled: boolean("rateLimitEnabled"),
|
||||
rateLimitTimeWindow: integer("rateLimitTimeWindow"),
|
||||
rateLimitMax: integer("rateLimitMax"),
|
||||
requestCount: integer("requestCount"),
|
||||
remaining: integer("remaining"),
|
||||
lastRequest: timestamp("lastRequest"),
|
||||
expiresAt: timestamp("expiresAt"),
|
||||
createdAt: timestamp("createdAt").notNull(),
|
||||
updatedAt: timestamp("updatedAt").notNull(),
|
||||
permissions: text("permissions"),
|
||||
metadata: text("metadata"),
|
||||
}).enableRLS();
|
||||
@@ -1,4 +1,6 @@
|
||||
export * from "./auth";
|
||||
export * from "./boards";
|
||||
export * from "./auth";
|
||||
export * from "./cards";
|
||||
export * from "./feedback";
|
||||
export * from "./imports";
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgTable, timestamp, uuid, varchar } from "drizzle-orm/pg-core";
|
||||
import { relations, sql } from "drizzle-orm";
|
||||
import {
|
||||
boolean,
|
||||
pgTable,
|
||||
timestamp,
|
||||
uuid,
|
||||
varchar,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
import { boards } from "./boards";
|
||||
import { cards } from "./cards";
|
||||
@@ -8,11 +14,16 @@ import { lists } from "./lists";
|
||||
import { workspaceMembers, workspaces } from "./workspaces";
|
||||
|
||||
export const users = pgTable("user", {
|
||||
id: uuid("id").notNull().primaryKey(),
|
||||
id: uuid("id")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.default(sql`uuid_generate_v4()`),
|
||||
name: varchar("name", { length: 255 }),
|
||||
email: varchar("email", { length: 255 }).notNull().unique(),
|
||||
emailVerified: timestamp("emailVerified", { mode: "date" }),
|
||||
emailVerified: boolean("emailVerified").notNull(),
|
||||
image: varchar("image", { length: 255 }),
|
||||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||
stripeCustomerId: varchar("stripeCustomerId", { length: 255 }),
|
||||
}).enableRLS();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user