feat: add personal access tokens (#159)
* feat: add personal access tokens * feat: replace MCP tokens with OAuth foundation * fix: keep OAuth constants private in auth foundation * fix: clean up mcp oauth branch scope * fix: expose oauth metadata endpoints * fix: trim mcp oauth config to non-default options Drop OIDC scopes, the org-id JWT claim, and the openid-configuration metadata endpoint since the MCP integration is OAuth-only and the org gets resolved server-side. Also remove options that just duplicated better-auth defaults. * fix: drop redundant oauth metadata helpers Remove `session.storeSessionInDatabase: true` since better-auth only enforces it when secondaryStorage is configured. Inline the `getHostedBaseUrlForOAuthMetadata` alias and skip the async `getOAuthServerConfig()` call in the protected-resource metadata handler — the issuer is just `baseURL` without a custom jwt.issuer override. * docs: explain cache headers on mcp metadata response * Use escaped file routes for OAuth metadata * save
This commit is contained in:
parent
fb265e3a3a
commit
6231424e88
@ -1,11 +1,12 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { betterAuth } from "better-auth";
|
import { betterAuth } from "better-auth";
|
||||||
import { baseAuthConfig } from "./src/lib/auth-config";
|
import { createBaseAuthConfig } from "./src/lib/auth-config";
|
||||||
|
|
||||||
const CLI_DEV_BASE_URL = "http://localhost:3000";
|
const CLI_DEV_BASE_URL = "http://localhost:3000";
|
||||||
|
const baseUrl = process.env.BETTER_AUTH_URL ?? CLI_DEV_BASE_URL;
|
||||||
|
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
baseURL: process.env.BETTER_AUTH_URL ?? CLI_DEV_BASE_URL,
|
baseURL: baseUrl,
|
||||||
secret: process.env.BETTER_AUTH_SECRET ?? randomUUID(),
|
secret: process.env.BETTER_AUTH_SECRET ?? randomUUID(),
|
||||||
...baseAuthConfig,
|
...createBaseAuthConfig(baseUrl),
|
||||||
});
|
});
|
||||||
|
|||||||
89
drizzle/0012_closed_impossible_man.sql
Normal file
89
drizzle/0012_closed_impossible_man.sql
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
CREATE TABLE `jwks` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`public_key` text NOT NULL,
|
||||||
|
`private_key` text NOT NULL,
|
||||||
|
`created_at` integer NOT NULL,
|
||||||
|
`expires_at` integer
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `oauth_access_token` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`token` text NOT NULL,
|
||||||
|
`client_id` text NOT NULL,
|
||||||
|
`session_id` text,
|
||||||
|
`user_id` text,
|
||||||
|
`reference_id` text,
|
||||||
|
`refresh_id` text,
|
||||||
|
`expires_at` integer NOT NULL,
|
||||||
|
`created_at` integer NOT NULL,
|
||||||
|
`scopes` text NOT NULL,
|
||||||
|
FOREIGN KEY (`client_id`) REFERENCES `oauth_client`(`client_id`) ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON UPDATE no action ON DELETE set null,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY (`refresh_id`) REFERENCES `oauth_refresh_token`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `oauth_access_token_token_unique` ON `oauth_access_token` (`token`);--> statement-breakpoint
|
||||||
|
CREATE TABLE `oauth_client` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`client_id` text NOT NULL,
|
||||||
|
`client_secret` text,
|
||||||
|
`disabled` integer DEFAULT false,
|
||||||
|
`skip_consent` integer,
|
||||||
|
`enable_end_session` integer,
|
||||||
|
`subject_type` text,
|
||||||
|
`scopes` text,
|
||||||
|
`user_id` text,
|
||||||
|
`created_at` integer,
|
||||||
|
`updated_at` integer,
|
||||||
|
`name` text,
|
||||||
|
`uri` text,
|
||||||
|
`icon` text,
|
||||||
|
`contacts` text,
|
||||||
|
`tos` text,
|
||||||
|
`policy` text,
|
||||||
|
`software_id` text,
|
||||||
|
`software_version` text,
|
||||||
|
`software_statement` text,
|
||||||
|
`redirect_uris` text NOT NULL,
|
||||||
|
`post_logout_redirect_uris` text,
|
||||||
|
`token_endpoint_auth_method` text,
|
||||||
|
`grant_types` text,
|
||||||
|
`response_types` text,
|
||||||
|
`public` integer,
|
||||||
|
`type` text,
|
||||||
|
`require_pkce` integer,
|
||||||
|
`reference_id` text,
|
||||||
|
`metadata` text,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `oauth_client_client_id_unique` ON `oauth_client` (`client_id`);--> statement-breakpoint
|
||||||
|
CREATE TABLE `oauth_consent` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`client_id` text NOT NULL,
|
||||||
|
`user_id` text,
|
||||||
|
`reference_id` text,
|
||||||
|
`scopes` text NOT NULL,
|
||||||
|
`created_at` integer NOT NULL,
|
||||||
|
`updated_at` integer NOT NULL,
|
||||||
|
FOREIGN KEY (`client_id`) REFERENCES `oauth_client`(`client_id`) ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `oauth_refresh_token` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`token` text NOT NULL,
|
||||||
|
`client_id` text NOT NULL,
|
||||||
|
`session_id` text,
|
||||||
|
`user_id` text NOT NULL,
|
||||||
|
`reference_id` text,
|
||||||
|
`expires_at` integer NOT NULL,
|
||||||
|
`created_at` integer NOT NULL,
|
||||||
|
`revoked` integer,
|
||||||
|
`auth_time` integer,
|
||||||
|
`scopes` text NOT NULL,
|
||||||
|
FOREIGN KEY (`client_id`) REFERENCES `oauth_client`(`client_id`) ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON UPDATE no action ON DELETE set null,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
2634
drizzle/meta/0012_snapshot.json
Normal file
2634
drizzle/meta/0012_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -85,6 +85,13 @@
|
|||||||
"when": 1778031161783,
|
"when": 1778031161783,
|
||||||
"tag": "0011_colorful_dark_beast",
|
"tag": "0011_colorful_dark_beast",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 12,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1778113978173,
|
||||||
|
"tag": "0012_closed_impossible_man",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -11,6 +11,7 @@
|
|||||||
"drizzle.config.ts",
|
"drizzle.config.ts",
|
||||||
// DB schema — exports consumed via `import * as schema` / drizzle()
|
// DB schema — exports consumed via `import * as schema` / drizzle()
|
||||||
"src/db/index.ts",
|
"src/db/index.ts",
|
||||||
|
"src/db/app.schema.ts",
|
||||||
"src/db/better-auth-schema.ts",
|
"src/db/better-auth-schema.ts",
|
||||||
],
|
],
|
||||||
"project": ["**/*.{js,mjs,ts,tsx}", "!src/routeTree.gen.ts", "!web/**"],
|
"project": ["**/*.{js,mjs,ts,tsx}", "!src/routeTree.gen.ts", "!web/**"],
|
||||||
|
|||||||
@ -49,6 +49,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@better-auth/oauth-provider": "^1.5.5",
|
||||||
"@every-app/sdk": "^0.1.14",
|
"@every-app/sdk": "^0.1.14",
|
||||||
"@tanstack/query-core": "^5.90.9",
|
"@tanstack/query-core": "^5.90.9",
|
||||||
"@tanstack/react-form": "^1.25.0",
|
"@tanstack/react-form": "^1.25.0",
|
||||||
|
|||||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@ -8,6 +8,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@better-auth/oauth-provider':
|
||||||
|
specifier: ^1.5.5
|
||||||
|
version: 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260302.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-auth@1.5.5(@cloudflare/workers-types@4.20260302.0)(@tanstack/react-start@1.167.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)))(drizzle-kit@0.31.9)(drizzle-orm@0.44.7(@cloudflare/workers-types@4.20260302.0)(@libsql/client@0.15.15)(@opentelemetry/api@1.9.1)(kysely@0.28.12))(mongodb@7.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)))(better-call@1.3.2(zod@4.3.6))
|
||||||
'@every-app/sdk':
|
'@every-app/sdk':
|
||||||
specifier: ^0.1.14
|
specifier: ^0.1.14
|
||||||
version: 0.1.14(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/react-start@1.167.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)))(jose@6.1.3)(react@19.2.4)
|
version: 0.1.14(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/react-start@1.167.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)))(jose@6.1.3)(react@19.2.4)
|
||||||
@ -325,6 +328,15 @@ packages:
|
|||||||
'@better-auth/utils': ^0.3.0
|
'@better-auth/utils': ^0.3.0
|
||||||
mongodb: ^6.0.0 || ^7.0.0
|
mongodb: ^6.0.0 || ^7.0.0
|
||||||
|
|
||||||
|
'@better-auth/oauth-provider@1.5.5':
|
||||||
|
resolution: {integrity: sha512-zH2uKtvd6406MysWCTBldPHTKCXEK8caMrNId03bh4ej4f2vU8+GfNGE+IyxARucHGI1T+Og7QrUgKAeA2jQUQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@better-auth/core': 1.5.5
|
||||||
|
'@better-auth/utils': 0.3.1
|
||||||
|
'@better-fetch/fetch': 1.1.21
|
||||||
|
better-auth: 1.5.5
|
||||||
|
better-call: 1.3.2
|
||||||
|
|
||||||
'@better-auth/prisma-adapter@1.5.5':
|
'@better-auth/prisma-adapter@1.5.5':
|
||||||
resolution: {integrity: sha512-CliDd78CXHzzwQIXhCdwGr5Ml53i6JdCHWV7PYwTIJz9EAm6qb2RVBdpP3nqEfNjINGM22A6gfleCgCdZkTIZg==}
|
resolution: {integrity: sha512-CliDd78CXHzzwQIXhCdwGr5Ml53i6JdCHWV7PYwTIJz9EAm6qb2RVBdpP3nqEfNjINGM22A6gfleCgCdZkTIZg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -4216,6 +4228,16 @@ snapshots:
|
|||||||
'@better-auth/utils': 0.3.1
|
'@better-auth/utils': 0.3.1
|
||||||
mongodb: 7.1.0
|
mongodb: 7.1.0
|
||||||
|
|
||||||
|
'@better-auth/oauth-provider@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260302.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-auth@1.5.5(@cloudflare/workers-types@4.20260302.0)(@tanstack/react-start@1.167.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)))(drizzle-kit@0.31.9)(drizzle-orm@0.44.7(@cloudflare/workers-types@4.20260302.0)(@libsql/client@0.15.15)(@opentelemetry/api@1.9.1)(kysely@0.28.12))(mongodb@7.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)))(better-call@1.3.2(zod@4.3.6))':
|
||||||
|
dependencies:
|
||||||
|
'@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260302.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1)
|
||||||
|
'@better-auth/utils': 0.3.1
|
||||||
|
'@better-fetch/fetch': 1.1.21
|
||||||
|
better-auth: 1.5.5(@cloudflare/workers-types@4.20260302.0)(@tanstack/react-start@1.167.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)))(drizzle-kit@0.31.9)(drizzle-orm@0.44.7(@cloudflare/workers-types@4.20260302.0)(@libsql/client@0.15.15)(@opentelemetry/api@1.9.1)(kysely@0.28.12))(mongodb@7.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))
|
||||||
|
better-call: 1.3.2(zod@4.3.6)
|
||||||
|
jose: 6.1.3
|
||||||
|
zod: 4.3.6
|
||||||
|
|
||||||
'@better-auth/prisma-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260302.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1))(@better-auth/utils@0.3.1)':
|
'@better-auth/prisma-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260302.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1))(@better-auth/utils@0.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260302.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1)
|
'@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260302.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1)
|
||||||
|
|||||||
@ -151,18 +151,116 @@ export const invitation = sqliteTable(
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const jwks = sqliteTable("jwks", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
publicKey: text("public_key").notNull(),
|
||||||
|
privateKey: text("private_key").notNull(),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
|
||||||
|
expiresAt: integer("expires_at", { mode: "timestamp_ms" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const oauthClient = sqliteTable("oauth_client", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
clientId: text("client_id").notNull().unique(),
|
||||||
|
clientSecret: text("client_secret"),
|
||||||
|
disabled: integer("disabled", { mode: "boolean" }).default(false),
|
||||||
|
skipConsent: integer("skip_consent", { mode: "boolean" }),
|
||||||
|
enableEndSession: integer("enable_end_session", { mode: "boolean" }),
|
||||||
|
subjectType: text("subject_type"),
|
||||||
|
scopes: text("scopes", { mode: "json" }),
|
||||||
|
userId: text("user_id").references(() => user.id, { onDelete: "cascade" }),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp_ms" }),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp_ms" }),
|
||||||
|
name: text("name"),
|
||||||
|
uri: text("uri"),
|
||||||
|
icon: text("icon"),
|
||||||
|
contacts: text("contacts", { mode: "json" }),
|
||||||
|
tos: text("tos"),
|
||||||
|
policy: text("policy"),
|
||||||
|
softwareId: text("software_id"),
|
||||||
|
softwareVersion: text("software_version"),
|
||||||
|
softwareStatement: text("software_statement"),
|
||||||
|
redirectUris: text("redirect_uris", { mode: "json" }).notNull(),
|
||||||
|
postLogoutRedirectUris: text("post_logout_redirect_uris", { mode: "json" }),
|
||||||
|
tokenEndpointAuthMethod: text("token_endpoint_auth_method"),
|
||||||
|
grantTypes: text("grant_types", { mode: "json" }),
|
||||||
|
responseTypes: text("response_types", { mode: "json" }),
|
||||||
|
public: integer("public", { mode: "boolean" }),
|
||||||
|
type: text("type"),
|
||||||
|
requirePKCE: integer("require_pkce", { mode: "boolean" }),
|
||||||
|
referenceId: text("reference_id"),
|
||||||
|
metadata: text("metadata", { mode: "json" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const oauthRefreshToken = sqliteTable("oauth_refresh_token", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
token: text("token").notNull(),
|
||||||
|
clientId: text("client_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => oauthClient.clientId, { onDelete: "cascade" }),
|
||||||
|
sessionId: text("session_id").references(() => session.id, {
|
||||||
|
onDelete: "set null",
|
||||||
|
}),
|
||||||
|
userId: text("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
|
referenceId: text("reference_id"),
|
||||||
|
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
|
||||||
|
revoked: integer("revoked", { mode: "timestamp_ms" }),
|
||||||
|
authTime: integer("auth_time", { mode: "timestamp_ms" }),
|
||||||
|
scopes: text("scopes", { mode: "json" }).notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const oauthAccessToken = sqliteTable("oauth_access_token", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
token: text("token").notNull().unique(),
|
||||||
|
clientId: text("client_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => oauthClient.clientId, { onDelete: "cascade" }),
|
||||||
|
sessionId: text("session_id").references(() => session.id, {
|
||||||
|
onDelete: "set null",
|
||||||
|
}),
|
||||||
|
userId: text("user_id").references(() => user.id, { onDelete: "cascade" }),
|
||||||
|
referenceId: text("reference_id"),
|
||||||
|
refreshId: text("refresh_id").references(() => oauthRefreshToken.id, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
}),
|
||||||
|
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
|
||||||
|
scopes: text("scopes", { mode: "json" }).notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const oauthConsent = sqliteTable("oauth_consent", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
clientId: text("client_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => oauthClient.clientId, { onDelete: "cascade" }),
|
||||||
|
userId: text("user_id").references(() => user.id, { onDelete: "cascade" }),
|
||||||
|
referenceId: text("reference_id"),
|
||||||
|
scopes: text("scopes", { mode: "json" }).notNull(),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
export const userRelations = relations(user, ({ many }) => ({
|
export const userRelations = relations(user, ({ many }) => ({
|
||||||
sessions: many(session),
|
sessions: many(session),
|
||||||
accounts: many(account),
|
accounts: many(account),
|
||||||
members: many(member),
|
members: many(member),
|
||||||
invitations: many(invitation),
|
invitations: many(invitation),
|
||||||
|
oauthClients: many(oauthClient),
|
||||||
|
oauthRefreshTokens: many(oauthRefreshToken),
|
||||||
|
oauthAccessTokens: many(oauthAccessToken),
|
||||||
|
oauthConsents: many(oauthConsent),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const sessionRelations = relations(session, ({ one }) => ({
|
export const sessionRelations = relations(session, ({ one, many }) => ({
|
||||||
user: one(user, {
|
user: one(user, {
|
||||||
fields: [session.userId],
|
fields: [session.userId],
|
||||||
references: [user.id],
|
references: [user.id],
|
||||||
}),
|
}),
|
||||||
|
oauthRefreshTokens: many(oauthRefreshToken),
|
||||||
|
oauthAccessTokens: many(oauthAccessToken),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const accountRelations = relations(account, ({ one }) => ({
|
export const accountRelations = relations(account, ({ one }) => ({
|
||||||
@ -198,3 +296,65 @@ export const invitationRelations = relations(invitation, ({ one }) => ({
|
|||||||
references: [user.id],
|
references: [user.id],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const oauthClientRelations = relations(oauthClient, ({ one, many }) => ({
|
||||||
|
user: one(user, {
|
||||||
|
fields: [oauthClient.userId],
|
||||||
|
references: [user.id],
|
||||||
|
}),
|
||||||
|
oauthRefreshTokens: many(oauthRefreshToken),
|
||||||
|
oauthAccessTokens: many(oauthAccessToken),
|
||||||
|
oauthConsents: many(oauthConsent),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const oauthRefreshTokenRelations = relations(
|
||||||
|
oauthRefreshToken,
|
||||||
|
({ one, many }) => ({
|
||||||
|
oauthClient: one(oauthClient, {
|
||||||
|
fields: [oauthRefreshToken.clientId],
|
||||||
|
references: [oauthClient.clientId],
|
||||||
|
}),
|
||||||
|
session: one(session, {
|
||||||
|
fields: [oauthRefreshToken.sessionId],
|
||||||
|
references: [session.id],
|
||||||
|
}),
|
||||||
|
user: one(user, {
|
||||||
|
fields: [oauthRefreshToken.userId],
|
||||||
|
references: [user.id],
|
||||||
|
}),
|
||||||
|
oauthAccessTokens: many(oauthAccessToken),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const oauthAccessTokenRelations = relations(
|
||||||
|
oauthAccessToken,
|
||||||
|
({ one }) => ({
|
||||||
|
oauthClient: one(oauthClient, {
|
||||||
|
fields: [oauthAccessToken.clientId],
|
||||||
|
references: [oauthClient.clientId],
|
||||||
|
}),
|
||||||
|
session: one(session, {
|
||||||
|
fields: [oauthAccessToken.sessionId],
|
||||||
|
references: [session.id],
|
||||||
|
}),
|
||||||
|
user: one(user, {
|
||||||
|
fields: [oauthAccessToken.userId],
|
||||||
|
references: [user.id],
|
||||||
|
}),
|
||||||
|
oauthRefreshToken: one(oauthRefreshToken, {
|
||||||
|
fields: [oauthAccessToken.refreshId],
|
||||||
|
references: [oauthRefreshToken.id],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const oauthConsentRelations = relations(oauthConsent, ({ one }) => ({
|
||||||
|
oauthClient: one(oauthClient, {
|
||||||
|
fields: [oauthConsent.clientId],
|
||||||
|
references: [oauthClient.clientId],
|
||||||
|
}),
|
||||||
|
user: one(user, {
|
||||||
|
fields: [oauthConsent.userId],
|
||||||
|
references: [user.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import {
|
|||||||
inferAdditionalFields,
|
inferAdditionalFields,
|
||||||
organizationClient,
|
organizationClient,
|
||||||
} from "better-auth/client/plugins";
|
} from "better-auth/client/plugins";
|
||||||
|
import { oauthProviderClient } from "@better-auth/oauth-provider/client";
|
||||||
import { captureClientEvent, resetAnalyticsUser } from "@/client/lib/posthog";
|
import { captureClientEvent, resetAnalyticsUser } from "@/client/lib/posthog";
|
||||||
import { userAdditionalFields } from "@/lib/auth-options";
|
import { userAdditionalFields } from "@/lib/auth-options";
|
||||||
import { getSignInHrefForLocation } from "@/lib/auth-redirect";
|
import { getSignInHrefForLocation } from "@/lib/auth-redirect";
|
||||||
@ -11,6 +12,7 @@ export const authClient = createAuthClient({
|
|||||||
baseURL: typeof window !== "undefined" ? window.location.origin : "",
|
baseURL: typeof window !== "undefined" ? window.location.origin : "",
|
||||||
plugins: [
|
plugins: [
|
||||||
organizationClient(),
|
organizationClient(),
|
||||||
|
oauthProviderClient(),
|
||||||
inferAdditionalFields({ user: userAdditionalFields }),
|
inferAdditionalFields({ user: userAdditionalFields }),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,29 @@
|
|||||||
import { organization } from "better-auth/plugins";
|
import { oauthProvider } from "@better-auth/oauth-provider";
|
||||||
|
import { jwt, organization } from "better-auth/plugins";
|
||||||
import { baseAuthOptions } from "@/lib/auth-options";
|
import { baseAuthOptions } from "@/lib/auth-options";
|
||||||
|
import { getMcpResource, MCP_SCOPE } from "@/lib/oauth-resource";
|
||||||
|
|
||||||
export const baseAuthConfig = {
|
export function createBaseAuthConfig(baseUrl: string) {
|
||||||
|
const mcpResource = getMcpResource(baseUrl);
|
||||||
|
|
||||||
|
return {
|
||||||
...baseAuthOptions,
|
...baseAuthOptions,
|
||||||
plugins: [organization()],
|
plugins: [
|
||||||
|
organization(),
|
||||||
|
jwt(),
|
||||||
|
oauthProvider({
|
||||||
|
loginPage: "/sign-in",
|
||||||
|
consentPage: "/oauth-consent",
|
||||||
|
signup: {
|
||||||
|
page: "/sign-up",
|
||||||
|
},
|
||||||
|
scopes: ["offline_access", MCP_SCOPE],
|
||||||
|
allowDynamicClientRegistration: true,
|
||||||
|
// TODO: drop once the MCP spec settles on a replacement for
|
||||||
|
// unauthenticated DCR — better-auth has flagged this option for removal.
|
||||||
|
allowUnauthenticatedClientRegistration: true,
|
||||||
|
validAudiences: [mcpResource],
|
||||||
|
}),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|||||||
import { tanstackStartCookies } from "better-auth/tanstack-start";
|
import { tanstackStartCookies } from "better-auth/tanstack-start";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { baseAuthConfig } from "@/lib/auth-config";
|
import { createBaseAuthConfig } from "@/lib/auth-config";
|
||||||
import { getOrCreateDefaultHostedOrganization } from "@/server/auth/default-hosted-organization";
|
import { getOrCreateDefaultHostedOrganization } from "@/server/auth/default-hosted-organization";
|
||||||
import {
|
import {
|
||||||
sendHostedPasswordResetEmail,
|
sendHostedPasswordResetEmail,
|
||||||
@ -25,6 +25,7 @@ const hostedBaseUrlSchema = z
|
|||||||
function createAuth() {
|
function createAuth() {
|
||||||
const baseUrl = getHostedBaseUrl();
|
const baseUrl = getHostedBaseUrl();
|
||||||
const bypassEmail = Reflect.get(env, "BYPASS_EMAIL_VERIFICATION") === "true";
|
const bypassEmail = Reflect.get(env, "BYPASS_EMAIL_VERIFICATION") === "true";
|
||||||
|
const baseAuthConfig = createBaseAuthConfig(baseUrl);
|
||||||
|
|
||||||
const auth = betterAuth({
|
const auth = betterAuth({
|
||||||
baseURL: baseUrl,
|
baseURL: baseUrl,
|
||||||
@ -102,7 +103,7 @@ function getTrustedOrigins(baseUrl: string) {
|
|||||||
return trustedOrigins;
|
return trustedOrigins;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHostedBaseUrl() {
|
export function getHostedBaseUrl() {
|
||||||
const baseUrl = env.BETTER_AUTH_URL?.trim();
|
const baseUrl = env.BETTER_AUTH_URL?.trim();
|
||||||
|
|
||||||
if (!baseUrl) {
|
if (!baseUrl) {
|
||||||
|
|||||||
6
src/lib/oauth-resource.ts
Normal file
6
src/lib/oauth-resource.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const MCP_RESOURCE_PATH = "/mcp";
|
||||||
|
export const MCP_SCOPE = "mcp";
|
||||||
|
|
||||||
|
export function getMcpResource(baseUrl: string) {
|
||||||
|
return new URL(MCP_RESOURCE_PATH, baseUrl).toString();
|
||||||
|
}
|
||||||
@ -18,14 +18,17 @@ import { Route as ProjectRouteRouteImport } from './routes/_project/route'
|
|||||||
import { Route as AppRouteRouteImport } from './routes/_app/route'
|
import { Route as AppRouteRouteImport } from './routes/_app/route'
|
||||||
import { Route as AppIndexRouteImport } from './routes/_app/index'
|
import { Route as AppIndexRouteImport } from './routes/_app/index'
|
||||||
import { Route as AuthenticatedSubscribeRouteImport } from './routes/_authenticated.subscribe'
|
import { Route as AuthenticatedSubscribeRouteImport } from './routes/_authenticated.subscribe'
|
||||||
|
import { Route as AuthenticatedOauthConsentRouteImport } from './routes/_authenticated.oauth-consent'
|
||||||
import { Route as AuthSignUpRouteImport } from './routes/_auth.sign-up'
|
import { Route as AuthSignUpRouteImport } from './routes/_auth.sign-up'
|
||||||
import { Route as AuthSignInRouteImport } from './routes/_auth.sign-in'
|
import { Route as AuthSignInRouteImport } from './routes/_auth.sign-in'
|
||||||
import { Route as AppSupportRouteImport } from './routes/_app/support'
|
import { Route as AppSupportRouteImport } from './routes/_app/support'
|
||||||
import { Route as AppSettingsRouteImport } from './routes/_app/settings'
|
import { Route as AppSettingsRouteImport } from './routes/_app/settings'
|
||||||
import { Route as AppBillingRouteImport } from './routes/_app/billing'
|
import { Route as AppBillingRouteImport } from './routes/_app/billing'
|
||||||
|
import { Route as DotwellKnownOauthAuthorizationServerRouteImport } from './routes/[.]well-known/oauth-authorization-server'
|
||||||
import { Route as ApiAutumnSplatRouteImport } from './routes/api/autumn/$'
|
import { Route as ApiAutumnSplatRouteImport } from './routes/api/autumn/$'
|
||||||
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
|
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
|
||||||
import { Route as AppHelpDataforseoApiKeyRouteImport } from './routes/_app/help/dataforseo-api-key'
|
import { Route as AppHelpDataforseoApiKeyRouteImport } from './routes/_app/help/dataforseo-api-key'
|
||||||
|
import { Route as DotwellKnownOauthProtectedResourceMcpRouteImport } from './routes/[.]well-known/oauth-protected-resource/mcp'
|
||||||
import { Route as ProjectPProjectIdRouteRouteImport } from './routes/_project/p/$projectId/route'
|
import { Route as ProjectPProjectIdRouteRouteImport } from './routes/_project/p/$projectId/route'
|
||||||
import { Route as ProjectPProjectIdIndexRouteImport } from './routes/_project/p/$projectId/index'
|
import { Route as ProjectPProjectIdIndexRouteImport } from './routes/_project/p/$projectId/index'
|
||||||
import { Route as ProjectPProjectIdSavedRouteImport } from './routes/_project/p/$projectId/saved'
|
import { Route as ProjectPProjectIdSavedRouteImport } from './routes/_project/p/$projectId/saved'
|
||||||
@ -83,6 +86,12 @@ const AuthenticatedSubscribeRoute = AuthenticatedSubscribeRouteImport.update({
|
|||||||
path: '/subscribe',
|
path: '/subscribe',
|
||||||
getParentRoute: () => AuthenticatedRoute,
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AuthenticatedOauthConsentRoute =
|
||||||
|
AuthenticatedOauthConsentRouteImport.update({
|
||||||
|
id: '/oauth-consent',
|
||||||
|
path: '/oauth-consent',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
const AuthSignUpRoute = AuthSignUpRouteImport.update({
|
const AuthSignUpRoute = AuthSignUpRouteImport.update({
|
||||||
id: '/sign-up',
|
id: '/sign-up',
|
||||||
path: '/sign-up',
|
path: '/sign-up',
|
||||||
@ -108,6 +117,12 @@ const AppBillingRoute = AppBillingRouteImport.update({
|
|||||||
path: '/billing',
|
path: '/billing',
|
||||||
getParentRoute: () => AppRouteRoute,
|
getParentRoute: () => AppRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const DotwellKnownOauthAuthorizationServerRoute =
|
||||||
|
DotwellKnownOauthAuthorizationServerRouteImport.update({
|
||||||
|
id: '/.well-known/oauth-authorization-server',
|
||||||
|
path: '/.well-known/oauth-authorization-server',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const ApiAutumnSplatRoute = ApiAutumnSplatRouteImport.update({
|
const ApiAutumnSplatRoute = ApiAutumnSplatRouteImport.update({
|
||||||
id: '/api/autumn/$',
|
id: '/api/autumn/$',
|
||||||
path: '/api/autumn/$',
|
path: '/api/autumn/$',
|
||||||
@ -123,6 +138,12 @@ const AppHelpDataforseoApiKeyRoute = AppHelpDataforseoApiKeyRouteImport.update({
|
|||||||
path: '/help/dataforseo-api-key',
|
path: '/help/dataforseo-api-key',
|
||||||
getParentRoute: () => AppRouteRoute,
|
getParentRoute: () => AppRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const DotwellKnownOauthProtectedResourceMcpRoute =
|
||||||
|
DotwellKnownOauthProtectedResourceMcpRouteImport.update({
|
||||||
|
id: '/.well-known/oauth-protected-resource/mcp',
|
||||||
|
path: '/.well-known/oauth-protected-resource/mcp',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const ProjectPProjectIdRouteRoute = ProjectPProjectIdRouteRouteImport.update({
|
const ProjectPProjectIdRouteRoute = ProjectPProjectIdRouteRouteImport.update({
|
||||||
id: '/p/$projectId',
|
id: '/p/$projectId',
|
||||||
path: '/p/$projectId',
|
path: '/p/$projectId',
|
||||||
@ -213,13 +234,16 @@ export interface FileRoutesByFullPath {
|
|||||||
'/forgot-password': typeof ForgotPasswordRoute
|
'/forgot-password': typeof ForgotPasswordRoute
|
||||||
'/reset-password': typeof ResetPasswordRoute
|
'/reset-password': typeof ResetPasswordRoute
|
||||||
'/verify-email': typeof VerifyEmailRoute
|
'/verify-email': typeof VerifyEmailRoute
|
||||||
|
'/.well-known/oauth-authorization-server': typeof DotwellKnownOauthAuthorizationServerRoute
|
||||||
'/billing': typeof AppBillingRoute
|
'/billing': typeof AppBillingRoute
|
||||||
'/settings': typeof AppSettingsRoute
|
'/settings': typeof AppSettingsRoute
|
||||||
'/support': typeof AppSupportRoute
|
'/support': typeof AppSupportRoute
|
||||||
'/sign-in': typeof AuthSignInRoute
|
'/sign-in': typeof AuthSignInRoute
|
||||||
'/sign-up': typeof AuthSignUpRoute
|
'/sign-up': typeof AuthSignUpRoute
|
||||||
|
'/oauth-consent': typeof AuthenticatedOauthConsentRoute
|
||||||
'/subscribe': typeof AuthenticatedSubscribeRoute
|
'/subscribe': typeof AuthenticatedSubscribeRoute
|
||||||
'/p/$projectId': typeof ProjectPProjectIdRouteRouteWithChildren
|
'/p/$projectId': typeof ProjectPProjectIdRouteRouteWithChildren
|
||||||
|
'/.well-known/oauth-protected-resource/mcp': typeof DotwellKnownOauthProtectedResourceMcpRoute
|
||||||
'/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
|
'/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
|
||||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
'/api/auth/$': typeof ApiAuthSplatRoute
|
||||||
'/api/autumn/$': typeof ApiAutumnSplatRoute
|
'/api/autumn/$': typeof ApiAutumnSplatRoute
|
||||||
@ -243,12 +267,15 @@ export interface FileRoutesByTo {
|
|||||||
'/forgot-password': typeof ForgotPasswordRoute
|
'/forgot-password': typeof ForgotPasswordRoute
|
||||||
'/reset-password': typeof ResetPasswordRoute
|
'/reset-password': typeof ResetPasswordRoute
|
||||||
'/verify-email': typeof VerifyEmailRoute
|
'/verify-email': typeof VerifyEmailRoute
|
||||||
|
'/.well-known/oauth-authorization-server': typeof DotwellKnownOauthAuthorizationServerRoute
|
||||||
'/billing': typeof AppBillingRoute
|
'/billing': typeof AppBillingRoute
|
||||||
'/settings': typeof AppSettingsRoute
|
'/settings': typeof AppSettingsRoute
|
||||||
'/support': typeof AppSupportRoute
|
'/support': typeof AppSupportRoute
|
||||||
'/sign-in': typeof AuthSignInRoute
|
'/sign-in': typeof AuthSignInRoute
|
||||||
'/sign-up': typeof AuthSignUpRoute
|
'/sign-up': typeof AuthSignUpRoute
|
||||||
|
'/oauth-consent': typeof AuthenticatedOauthConsentRoute
|
||||||
'/subscribe': typeof AuthenticatedSubscribeRoute
|
'/subscribe': typeof AuthenticatedSubscribeRoute
|
||||||
|
'/.well-known/oauth-protected-resource/mcp': typeof DotwellKnownOauthProtectedResourceMcpRoute
|
||||||
'/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
|
'/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
|
||||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
'/api/auth/$': typeof ApiAuthSplatRoute
|
||||||
'/api/autumn/$': typeof ApiAutumnSplatRoute
|
'/api/autumn/$': typeof ApiAutumnSplatRoute
|
||||||
@ -274,14 +301,17 @@ export interface FileRoutesById {
|
|||||||
'/forgot-password': typeof ForgotPasswordRoute
|
'/forgot-password': typeof ForgotPasswordRoute
|
||||||
'/reset-password': typeof ResetPasswordRoute
|
'/reset-password': typeof ResetPasswordRoute
|
||||||
'/verify-email': typeof VerifyEmailRoute
|
'/verify-email': typeof VerifyEmailRoute
|
||||||
|
'/.well-known/oauth-authorization-server': typeof DotwellKnownOauthAuthorizationServerRoute
|
||||||
'/_app/billing': typeof AppBillingRoute
|
'/_app/billing': typeof AppBillingRoute
|
||||||
'/_app/settings': typeof AppSettingsRoute
|
'/_app/settings': typeof AppSettingsRoute
|
||||||
'/_app/support': typeof AppSupportRoute
|
'/_app/support': typeof AppSupportRoute
|
||||||
'/_auth/sign-in': typeof AuthSignInRoute
|
'/_auth/sign-in': typeof AuthSignInRoute
|
||||||
'/_auth/sign-up': typeof AuthSignUpRoute
|
'/_auth/sign-up': typeof AuthSignUpRoute
|
||||||
|
'/_authenticated/oauth-consent': typeof AuthenticatedOauthConsentRoute
|
||||||
'/_authenticated/subscribe': typeof AuthenticatedSubscribeRoute
|
'/_authenticated/subscribe': typeof AuthenticatedSubscribeRoute
|
||||||
'/_app/': typeof AppIndexRoute
|
'/_app/': typeof AppIndexRoute
|
||||||
'/_project/p/$projectId': typeof ProjectPProjectIdRouteRouteWithChildren
|
'/_project/p/$projectId': typeof ProjectPProjectIdRouteRouteWithChildren
|
||||||
|
'/.well-known/oauth-protected-resource/mcp': typeof DotwellKnownOauthProtectedResourceMcpRoute
|
||||||
'/_app/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
|
'/_app/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
|
||||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
'/api/auth/$': typeof ApiAuthSplatRoute
|
||||||
'/api/autumn/$': typeof ApiAutumnSplatRoute
|
'/api/autumn/$': typeof ApiAutumnSplatRoute
|
||||||
@ -307,13 +337,16 @@ export interface FileRouteTypes {
|
|||||||
| '/forgot-password'
|
| '/forgot-password'
|
||||||
| '/reset-password'
|
| '/reset-password'
|
||||||
| '/verify-email'
|
| '/verify-email'
|
||||||
|
| '/.well-known/oauth-authorization-server'
|
||||||
| '/billing'
|
| '/billing'
|
||||||
| '/settings'
|
| '/settings'
|
||||||
| '/support'
|
| '/support'
|
||||||
| '/sign-in'
|
| '/sign-in'
|
||||||
| '/sign-up'
|
| '/sign-up'
|
||||||
|
| '/oauth-consent'
|
||||||
| '/subscribe'
|
| '/subscribe'
|
||||||
| '/p/$projectId'
|
| '/p/$projectId'
|
||||||
|
| '/.well-known/oauth-protected-resource/mcp'
|
||||||
| '/help/dataforseo-api-key'
|
| '/help/dataforseo-api-key'
|
||||||
| '/api/auth/$'
|
| '/api/auth/$'
|
||||||
| '/api/autumn/$'
|
| '/api/autumn/$'
|
||||||
@ -337,12 +370,15 @@ export interface FileRouteTypes {
|
|||||||
| '/forgot-password'
|
| '/forgot-password'
|
||||||
| '/reset-password'
|
| '/reset-password'
|
||||||
| '/verify-email'
|
| '/verify-email'
|
||||||
|
| '/.well-known/oauth-authorization-server'
|
||||||
| '/billing'
|
| '/billing'
|
||||||
| '/settings'
|
| '/settings'
|
||||||
| '/support'
|
| '/support'
|
||||||
| '/sign-in'
|
| '/sign-in'
|
||||||
| '/sign-up'
|
| '/sign-up'
|
||||||
|
| '/oauth-consent'
|
||||||
| '/subscribe'
|
| '/subscribe'
|
||||||
|
| '/.well-known/oauth-protected-resource/mcp'
|
||||||
| '/help/dataforseo-api-key'
|
| '/help/dataforseo-api-key'
|
||||||
| '/api/auth/$'
|
| '/api/auth/$'
|
||||||
| '/api/autumn/$'
|
| '/api/autumn/$'
|
||||||
@ -367,14 +403,17 @@ export interface FileRouteTypes {
|
|||||||
| '/forgot-password'
|
| '/forgot-password'
|
||||||
| '/reset-password'
|
| '/reset-password'
|
||||||
| '/verify-email'
|
| '/verify-email'
|
||||||
|
| '/.well-known/oauth-authorization-server'
|
||||||
| '/_app/billing'
|
| '/_app/billing'
|
||||||
| '/_app/settings'
|
| '/_app/settings'
|
||||||
| '/_app/support'
|
| '/_app/support'
|
||||||
| '/_auth/sign-in'
|
| '/_auth/sign-in'
|
||||||
| '/_auth/sign-up'
|
| '/_auth/sign-up'
|
||||||
|
| '/_authenticated/oauth-consent'
|
||||||
| '/_authenticated/subscribe'
|
| '/_authenticated/subscribe'
|
||||||
| '/_app/'
|
| '/_app/'
|
||||||
| '/_project/p/$projectId'
|
| '/_project/p/$projectId'
|
||||||
|
| '/.well-known/oauth-protected-resource/mcp'
|
||||||
| '/_app/help/dataforseo-api-key'
|
| '/_app/help/dataforseo-api-key'
|
||||||
| '/api/auth/$'
|
| '/api/auth/$'
|
||||||
| '/api/autumn/$'
|
| '/api/autumn/$'
|
||||||
@ -402,6 +441,8 @@ export interface RootRouteChildren {
|
|||||||
ForgotPasswordRoute: typeof ForgotPasswordRoute
|
ForgotPasswordRoute: typeof ForgotPasswordRoute
|
||||||
ResetPasswordRoute: typeof ResetPasswordRoute
|
ResetPasswordRoute: typeof ResetPasswordRoute
|
||||||
VerifyEmailRoute: typeof VerifyEmailRoute
|
VerifyEmailRoute: typeof VerifyEmailRoute
|
||||||
|
DotwellKnownOauthAuthorizationServerRoute: typeof DotwellKnownOauthAuthorizationServerRoute
|
||||||
|
DotwellKnownOauthProtectedResourceMcpRoute: typeof DotwellKnownOauthProtectedResourceMcpRoute
|
||||||
ApiAuthSplatRoute: typeof ApiAuthSplatRoute
|
ApiAuthSplatRoute: typeof ApiAuthSplatRoute
|
||||||
ApiAutumnSplatRoute: typeof ApiAutumnSplatRoute
|
ApiAutumnSplatRoute: typeof ApiAutumnSplatRoute
|
||||||
}
|
}
|
||||||
@ -471,6 +512,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AuthenticatedSubscribeRouteImport
|
preLoaderRoute: typeof AuthenticatedSubscribeRouteImport
|
||||||
parentRoute: typeof AuthenticatedRoute
|
parentRoute: typeof AuthenticatedRoute
|
||||||
}
|
}
|
||||||
|
'/_authenticated/oauth-consent': {
|
||||||
|
id: '/_authenticated/oauth-consent'
|
||||||
|
path: '/oauth-consent'
|
||||||
|
fullPath: '/oauth-consent'
|
||||||
|
preLoaderRoute: typeof AuthenticatedOauthConsentRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
'/_auth/sign-up': {
|
'/_auth/sign-up': {
|
||||||
id: '/_auth/sign-up'
|
id: '/_auth/sign-up'
|
||||||
path: '/sign-up'
|
path: '/sign-up'
|
||||||
@ -506,6 +554,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AppBillingRouteImport
|
preLoaderRoute: typeof AppBillingRouteImport
|
||||||
parentRoute: typeof AppRouteRoute
|
parentRoute: typeof AppRouteRoute
|
||||||
}
|
}
|
||||||
|
'/.well-known/oauth-authorization-server': {
|
||||||
|
id: '/.well-known/oauth-authorization-server'
|
||||||
|
path: '/.well-known/oauth-authorization-server'
|
||||||
|
fullPath: '/.well-known/oauth-authorization-server'
|
||||||
|
preLoaderRoute: typeof DotwellKnownOauthAuthorizationServerRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/api/autumn/$': {
|
'/api/autumn/$': {
|
||||||
id: '/api/autumn/$'
|
id: '/api/autumn/$'
|
||||||
path: '/api/autumn/$'
|
path: '/api/autumn/$'
|
||||||
@ -527,6 +582,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AppHelpDataforseoApiKeyRouteImport
|
preLoaderRoute: typeof AppHelpDataforseoApiKeyRouteImport
|
||||||
parentRoute: typeof AppRouteRoute
|
parentRoute: typeof AppRouteRoute
|
||||||
}
|
}
|
||||||
|
'/.well-known/oauth-protected-resource/mcp': {
|
||||||
|
id: '/.well-known/oauth-protected-resource/mcp'
|
||||||
|
path: '/.well-known/oauth-protected-resource/mcp'
|
||||||
|
fullPath: '/.well-known/oauth-protected-resource/mcp'
|
||||||
|
preLoaderRoute: typeof DotwellKnownOauthProtectedResourceMcpRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/_project/p/$projectId': {
|
'/_project/p/$projectId': {
|
||||||
id: '/_project/p/$projectId'
|
id: '/_project/p/$projectId'
|
||||||
path: '/p/$projectId'
|
path: '/p/$projectId'
|
||||||
@ -748,10 +810,12 @@ const AuthRouteChildren: AuthRouteChildren = {
|
|||||||
const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
|
const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
|
||||||
|
|
||||||
interface AuthenticatedRouteChildren {
|
interface AuthenticatedRouteChildren {
|
||||||
|
AuthenticatedOauthConsentRoute: typeof AuthenticatedOauthConsentRoute
|
||||||
AuthenticatedSubscribeRoute: typeof AuthenticatedSubscribeRoute
|
AuthenticatedSubscribeRoute: typeof AuthenticatedSubscribeRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
|
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
|
||||||
|
AuthenticatedOauthConsentRoute: AuthenticatedOauthConsentRoute,
|
||||||
AuthenticatedSubscribeRoute: AuthenticatedSubscribeRoute,
|
AuthenticatedSubscribeRoute: AuthenticatedSubscribeRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -767,6 +831,10 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
ForgotPasswordRoute: ForgotPasswordRoute,
|
ForgotPasswordRoute: ForgotPasswordRoute,
|
||||||
ResetPasswordRoute: ResetPasswordRoute,
|
ResetPasswordRoute: ResetPasswordRoute,
|
||||||
VerifyEmailRoute: VerifyEmailRoute,
|
VerifyEmailRoute: VerifyEmailRoute,
|
||||||
|
DotwellKnownOauthAuthorizationServerRoute:
|
||||||
|
DotwellKnownOauthAuthorizationServerRoute,
|
||||||
|
DotwellKnownOauthProtectedResourceMcpRoute:
|
||||||
|
DotwellKnownOauthProtectedResourceMcpRoute,
|
||||||
ApiAuthSplatRoute: ApiAuthSplatRoute,
|
ApiAuthSplatRoute: ApiAuthSplatRoute,
|
||||||
ApiAutumnSplatRoute: ApiAutumnSplatRoute,
|
ApiAutumnSplatRoute: ApiAutumnSplatRoute,
|
||||||
}
|
}
|
||||||
|
|||||||
31
src/routes/[.]well-known/oauth-authorization-server.ts
Normal file
31
src/routes/[.]well-known/oauth-authorization-server.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { oauthProviderAuthServerMetadata } from "@better-auth/oauth-provider";
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { env } from "cloudflare:workers";
|
||||||
|
import { getAuth, hasHostedAuthConfig } from "@/lib/auth";
|
||||||
|
import { isHostedAuthMode } from "@/lib/auth-mode";
|
||||||
|
|
||||||
|
function unavailableMetadataResponse() {
|
||||||
|
if (!isHostedAuthMode(env.AUTH_MODE)) {
|
||||||
|
return new Response("Not found", { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response("Missing Better Auth hosted configuration", {
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/.well-known/oauth-authorization-server")(
|
||||||
|
{
|
||||||
|
server: {
|
||||||
|
handlers: {
|
||||||
|
GET: async ({ request }: { request: Request }) => {
|
||||||
|
if (!isHostedAuthMode(env.AUTH_MODE) || !hasHostedAuthConfig()) {
|
||||||
|
return unavailableMetadataResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
return oauthProviderAuthServerMetadata(getAuth())(request);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
46
src/routes/[.]well-known/oauth-protected-resource/mcp.ts
Normal file
46
src/routes/[.]well-known/oauth-protected-resource/mcp.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { env } from "cloudflare:workers";
|
||||||
|
import { getAuth, getHostedBaseUrl, hasHostedAuthConfig } from "@/lib/auth";
|
||||||
|
import { isHostedAuthMode } from "@/lib/auth-mode";
|
||||||
|
import { getMcpResource, MCP_SCOPE } from "@/lib/oauth-resource";
|
||||||
|
|
||||||
|
function unavailableMetadataResponse() {
|
||||||
|
if (!isHostedAuthMode(env.AUTH_MODE)) {
|
||||||
|
return new Response("Not found", { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response("Missing Better Auth hosted configuration", {
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/.well-known/oauth-protected-resource/mcp",
|
||||||
|
)({
|
||||||
|
server: {
|
||||||
|
handlers: {
|
||||||
|
GET: async () => {
|
||||||
|
if (!isHostedAuthMode(env.AUTH_MODE) || !hasHostedAuthConfig()) {
|
||||||
|
return unavailableMetadataResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = getHostedBaseUrl();
|
||||||
|
const authServerMetadata = await getAuth().api.getOAuthServerConfig();
|
||||||
|
const metadata = {
|
||||||
|
resource: getMcpResource(baseUrl),
|
||||||
|
authorization_servers: [authServerMetadata.issuer],
|
||||||
|
scopes_supported: [MCP_SCOPE],
|
||||||
|
resource_name: "OpenSEO MCP",
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(metadata), {
|
||||||
|
headers: {
|
||||||
|
"Cache-Control":
|
||||||
|
"public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
73
src/routes/_authenticated.oauth-consent.tsx
Normal file
73
src/routes/_authenticated.oauth-consent.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { ShieldCheck } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_authenticated/oauth-consent")({
|
||||||
|
component: OAuthConsentPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
function OAuthConsentPage() {
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
async function respond(accept: boolean) {
|
||||||
|
setError(null);
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
const { data, error: consentError } = await authClient.oauth2.consent({
|
||||||
|
accept,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (consentError) {
|
||||||
|
setError(consentError.message ?? "Unable to complete authorization.");
|
||||||
|
setIsSubmitting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.redirect && data.url) {
|
||||||
|
window.location.assign(data.url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setError("Authorization response did not include a redirect URL.");
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-sm space-y-5">
|
||||||
|
<div className="text-center space-y-3">
|
||||||
|
<div className="mx-auto flex size-12 items-center justify-center rounded-lg bg-base-200">
|
||||||
|
<ShieldCheck className="size-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-semibold">Authorize MCP access</h1>
|
||||||
|
<p className="mt-2 text-sm text-base-content/70">
|
||||||
|
Allow this MCP client to access your OpenSEO workspace.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error ? <p className="text-sm text-error">{error}</p> : null}
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost flex-1"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onClick={() => void respond(false)}
|
||||||
|
>
|
||||||
|
Deny
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary flex-1"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onClick={() => void respond(true)}
|
||||||
|
>
|
||||||
|
{isSubmitting ? "Authorizing..." : "Authorize"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -18,7 +18,9 @@ function AuthenticatedShellLayout() {
|
|||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
void navigate({
|
void navigate({
|
||||||
to: "/sign-in",
|
to: "/sign-in",
|
||||||
search: { redirect: window.location.pathname },
|
search: {
|
||||||
|
redirect: `${window.location.pathname}${window.location.search}`,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isPending, isHostedMode, session?.user?.id, navigate]);
|
}, [isPending, isHostedMode, session?.user?.id, navigate]);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user