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 { 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 baseUrl = process.env.BETTER_AUTH_URL ?? CLI_DEV_BASE_URL;
|
||||
|
||||
export const auth = betterAuth({
|
||||
baseURL: process.env.BETTER_AUTH_URL ?? CLI_DEV_BASE_URL,
|
||||
baseURL: baseUrl,
|
||||
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,
|
||||
"tag": "0011_colorful_dark_beast",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 12,
|
||||
"version": "6",
|
||||
"when": 1778113978173,
|
||||
"tag": "0012_closed_impossible_man",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -11,6 +11,7 @@
|
||||
"drizzle.config.ts",
|
||||
// DB schema — exports consumed via `import * as schema` / drizzle()
|
||||
"src/db/index.ts",
|
||||
"src/db/app.schema.ts",
|
||||
"src/db/better-auth-schema.ts",
|
||||
],
|
||||
"project": ["**/*.{js,mjs,ts,tsx}", "!src/routeTree.gen.ts", "!web/**"],
|
||||
|
||||
@ -49,6 +49,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@better-auth/oauth-provider": "^1.5.5",
|
||||
"@every-app/sdk": "^0.1.14",
|
||||
"@tanstack/query-core": "^5.90.9",
|
||||
"@tanstack/react-form": "^1.25.0",
|
||||
|
||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@ -8,6 +8,9 @@ importers:
|
||||
|
||||
.:
|
||||
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':
|
||||
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)
|
||||
@ -325,6 +328,15 @@ packages:
|
||||
'@better-auth/utils': ^0.3.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':
|
||||
resolution: {integrity: sha512-CliDd78CXHzzwQIXhCdwGr5Ml53i6JdCHWV7PYwTIJz9EAm6qb2RVBdpP3nqEfNjINGM22A6gfleCgCdZkTIZg==}
|
||||
peerDependencies:
|
||||
@ -4216,6 +4228,16 @@ snapshots:
|
||||
'@better-auth/utils': 0.3.1
|
||||
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)':
|
||||
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)
|
||||
|
||||
@ -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 }) => ({
|
||||
sessions: many(session),
|
||||
accounts: many(account),
|
||||
members: many(member),
|
||||
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, {
|
||||
fields: [session.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
oauthRefreshTokens: many(oauthRefreshToken),
|
||||
oauthAccessTokens: many(oauthAccessToken),
|
||||
}));
|
||||
|
||||
export const accountRelations = relations(account, ({ one }) => ({
|
||||
@ -198,3 +296,65 @@ export const invitationRelations = relations(invitation, ({ one }) => ({
|
||||
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,
|
||||
organizationClient,
|
||||
} from "better-auth/client/plugins";
|
||||
import { oauthProviderClient } from "@better-auth/oauth-provider/client";
|
||||
import { captureClientEvent, resetAnalyticsUser } from "@/client/lib/posthog";
|
||||
import { userAdditionalFields } from "@/lib/auth-options";
|
||||
import { getSignInHrefForLocation } from "@/lib/auth-redirect";
|
||||
@ -11,6 +12,7 @@ export const authClient = createAuthClient({
|
||||
baseURL: typeof window !== "undefined" ? window.location.origin : "",
|
||||
plugins: [
|
||||
organizationClient(),
|
||||
oauthProviderClient(),
|
||||
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 { getMcpResource, MCP_SCOPE } from "@/lib/oauth-resource";
|
||||
|
||||
export const baseAuthConfig = {
|
||||
export function createBaseAuthConfig(baseUrl: string) {
|
||||
const mcpResource = getMcpResource(baseUrl);
|
||||
|
||||
return {
|
||||
...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 { db } from "@/db";
|
||||
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 {
|
||||
sendHostedPasswordResetEmail,
|
||||
@ -25,6 +25,7 @@ const hostedBaseUrlSchema = z
|
||||
function createAuth() {
|
||||
const baseUrl = getHostedBaseUrl();
|
||||
const bypassEmail = Reflect.get(env, "BYPASS_EMAIL_VERIFICATION") === "true";
|
||||
const baseAuthConfig = createBaseAuthConfig(baseUrl);
|
||||
|
||||
const auth = betterAuth({
|
||||
baseURL: baseUrl,
|
||||
@ -102,7 +103,7 @@ function getTrustedOrigins(baseUrl: string) {
|
||||
return trustedOrigins;
|
||||
}
|
||||
|
||||
function getHostedBaseUrl() {
|
||||
export function getHostedBaseUrl() {
|
||||
const baseUrl = env.BETTER_AUTH_URL?.trim();
|
||||
|
||||
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 AppIndexRouteImport } from './routes/_app/index'
|
||||
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 AuthSignInRouteImport } from './routes/_auth.sign-in'
|
||||
import { Route as AppSupportRouteImport } from './routes/_app/support'
|
||||
import { Route as AppSettingsRouteImport } from './routes/_app/settings'
|
||||
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 ApiAuthSplatRouteImport } from './routes/api/auth/$'
|
||||
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 ProjectPProjectIdIndexRouteImport } from './routes/_project/p/$projectId/index'
|
||||
import { Route as ProjectPProjectIdSavedRouteImport } from './routes/_project/p/$projectId/saved'
|
||||
@ -83,6 +86,12 @@ const AuthenticatedSubscribeRoute = AuthenticatedSubscribeRouteImport.update({
|
||||
path: '/subscribe',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedOauthConsentRoute =
|
||||
AuthenticatedOauthConsentRouteImport.update({
|
||||
id: '/oauth-consent',
|
||||
path: '/oauth-consent',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthSignUpRoute = AuthSignUpRouteImport.update({
|
||||
id: '/sign-up',
|
||||
path: '/sign-up',
|
||||
@ -108,6 +117,12 @@ const AppBillingRoute = AppBillingRouteImport.update({
|
||||
path: '/billing',
|
||||
getParentRoute: () => AppRouteRoute,
|
||||
} 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({
|
||||
id: '/api/autumn/$',
|
||||
path: '/api/autumn/$',
|
||||
@ -123,6 +138,12 @@ const AppHelpDataforseoApiKeyRoute = AppHelpDataforseoApiKeyRouteImport.update({
|
||||
path: '/help/dataforseo-api-key',
|
||||
getParentRoute: () => AppRouteRoute,
|
||||
} 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({
|
||||
id: '/p/$projectId',
|
||||
path: '/p/$projectId',
|
||||
@ -213,13 +234,16 @@ export interface FileRoutesByFullPath {
|
||||
'/forgot-password': typeof ForgotPasswordRoute
|
||||
'/reset-password': typeof ResetPasswordRoute
|
||||
'/verify-email': typeof VerifyEmailRoute
|
||||
'/.well-known/oauth-authorization-server': typeof DotwellKnownOauthAuthorizationServerRoute
|
||||
'/billing': typeof AppBillingRoute
|
||||
'/settings': typeof AppSettingsRoute
|
||||
'/support': typeof AppSupportRoute
|
||||
'/sign-in': typeof AuthSignInRoute
|
||||
'/sign-up': typeof AuthSignUpRoute
|
||||
'/oauth-consent': typeof AuthenticatedOauthConsentRoute
|
||||
'/subscribe': typeof AuthenticatedSubscribeRoute
|
||||
'/p/$projectId': typeof ProjectPProjectIdRouteRouteWithChildren
|
||||
'/.well-known/oauth-protected-resource/mcp': typeof DotwellKnownOauthProtectedResourceMcpRoute
|
||||
'/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
|
||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
||||
'/api/autumn/$': typeof ApiAutumnSplatRoute
|
||||
@ -243,12 +267,15 @@ export interface FileRoutesByTo {
|
||||
'/forgot-password': typeof ForgotPasswordRoute
|
||||
'/reset-password': typeof ResetPasswordRoute
|
||||
'/verify-email': typeof VerifyEmailRoute
|
||||
'/.well-known/oauth-authorization-server': typeof DotwellKnownOauthAuthorizationServerRoute
|
||||
'/billing': typeof AppBillingRoute
|
||||
'/settings': typeof AppSettingsRoute
|
||||
'/support': typeof AppSupportRoute
|
||||
'/sign-in': typeof AuthSignInRoute
|
||||
'/sign-up': typeof AuthSignUpRoute
|
||||
'/oauth-consent': typeof AuthenticatedOauthConsentRoute
|
||||
'/subscribe': typeof AuthenticatedSubscribeRoute
|
||||
'/.well-known/oauth-protected-resource/mcp': typeof DotwellKnownOauthProtectedResourceMcpRoute
|
||||
'/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
|
||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
||||
'/api/autumn/$': typeof ApiAutumnSplatRoute
|
||||
@ -274,14 +301,17 @@ export interface FileRoutesById {
|
||||
'/forgot-password': typeof ForgotPasswordRoute
|
||||
'/reset-password': typeof ResetPasswordRoute
|
||||
'/verify-email': typeof VerifyEmailRoute
|
||||
'/.well-known/oauth-authorization-server': typeof DotwellKnownOauthAuthorizationServerRoute
|
||||
'/_app/billing': typeof AppBillingRoute
|
||||
'/_app/settings': typeof AppSettingsRoute
|
||||
'/_app/support': typeof AppSupportRoute
|
||||
'/_auth/sign-in': typeof AuthSignInRoute
|
||||
'/_auth/sign-up': typeof AuthSignUpRoute
|
||||
'/_authenticated/oauth-consent': typeof AuthenticatedOauthConsentRoute
|
||||
'/_authenticated/subscribe': typeof AuthenticatedSubscribeRoute
|
||||
'/_app/': typeof AppIndexRoute
|
||||
'/_project/p/$projectId': typeof ProjectPProjectIdRouteRouteWithChildren
|
||||
'/.well-known/oauth-protected-resource/mcp': typeof DotwellKnownOauthProtectedResourceMcpRoute
|
||||
'/_app/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
|
||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
||||
'/api/autumn/$': typeof ApiAutumnSplatRoute
|
||||
@ -307,13 +337,16 @@ export interface FileRouteTypes {
|
||||
| '/forgot-password'
|
||||
| '/reset-password'
|
||||
| '/verify-email'
|
||||
| '/.well-known/oauth-authorization-server'
|
||||
| '/billing'
|
||||
| '/settings'
|
||||
| '/support'
|
||||
| '/sign-in'
|
||||
| '/sign-up'
|
||||
| '/oauth-consent'
|
||||
| '/subscribe'
|
||||
| '/p/$projectId'
|
||||
| '/.well-known/oauth-protected-resource/mcp'
|
||||
| '/help/dataforseo-api-key'
|
||||
| '/api/auth/$'
|
||||
| '/api/autumn/$'
|
||||
@ -337,12 +370,15 @@ export interface FileRouteTypes {
|
||||
| '/forgot-password'
|
||||
| '/reset-password'
|
||||
| '/verify-email'
|
||||
| '/.well-known/oauth-authorization-server'
|
||||
| '/billing'
|
||||
| '/settings'
|
||||
| '/support'
|
||||
| '/sign-in'
|
||||
| '/sign-up'
|
||||
| '/oauth-consent'
|
||||
| '/subscribe'
|
||||
| '/.well-known/oauth-protected-resource/mcp'
|
||||
| '/help/dataforseo-api-key'
|
||||
| '/api/auth/$'
|
||||
| '/api/autumn/$'
|
||||
@ -367,14 +403,17 @@ export interface FileRouteTypes {
|
||||
| '/forgot-password'
|
||||
| '/reset-password'
|
||||
| '/verify-email'
|
||||
| '/.well-known/oauth-authorization-server'
|
||||
| '/_app/billing'
|
||||
| '/_app/settings'
|
||||
| '/_app/support'
|
||||
| '/_auth/sign-in'
|
||||
| '/_auth/sign-up'
|
||||
| '/_authenticated/oauth-consent'
|
||||
| '/_authenticated/subscribe'
|
||||
| '/_app/'
|
||||
| '/_project/p/$projectId'
|
||||
| '/.well-known/oauth-protected-resource/mcp'
|
||||
| '/_app/help/dataforseo-api-key'
|
||||
| '/api/auth/$'
|
||||
| '/api/autumn/$'
|
||||
@ -402,6 +441,8 @@ export interface RootRouteChildren {
|
||||
ForgotPasswordRoute: typeof ForgotPasswordRoute
|
||||
ResetPasswordRoute: typeof ResetPasswordRoute
|
||||
VerifyEmailRoute: typeof VerifyEmailRoute
|
||||
DotwellKnownOauthAuthorizationServerRoute: typeof DotwellKnownOauthAuthorizationServerRoute
|
||||
DotwellKnownOauthProtectedResourceMcpRoute: typeof DotwellKnownOauthProtectedResourceMcpRoute
|
||||
ApiAuthSplatRoute: typeof ApiAuthSplatRoute
|
||||
ApiAutumnSplatRoute: typeof ApiAutumnSplatRoute
|
||||
}
|
||||
@ -471,6 +512,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthenticatedSubscribeRouteImport
|
||||
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': {
|
||||
id: '/_auth/sign-up'
|
||||
path: '/sign-up'
|
||||
@ -506,6 +554,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AppBillingRouteImport
|
||||
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/$': {
|
||||
id: '/api/autumn/$'
|
||||
path: '/api/autumn/$'
|
||||
@ -527,6 +582,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AppHelpDataforseoApiKeyRouteImport
|
||||
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': {
|
||||
id: '/_project/p/$projectId'
|
||||
path: '/p/$projectId'
|
||||
@ -748,10 +810,12 @@ const AuthRouteChildren: AuthRouteChildren = {
|
||||
const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
|
||||
|
||||
interface AuthenticatedRouteChildren {
|
||||
AuthenticatedOauthConsentRoute: typeof AuthenticatedOauthConsentRoute
|
||||
AuthenticatedSubscribeRoute: typeof AuthenticatedSubscribeRoute
|
||||
}
|
||||
|
||||
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
|
||||
AuthenticatedOauthConsentRoute: AuthenticatedOauthConsentRoute,
|
||||
AuthenticatedSubscribeRoute: AuthenticatedSubscribeRoute,
|
||||
}
|
||||
|
||||
@ -767,6 +831,10 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
ForgotPasswordRoute: ForgotPasswordRoute,
|
||||
ResetPasswordRoute: ResetPasswordRoute,
|
||||
VerifyEmailRoute: VerifyEmailRoute,
|
||||
DotwellKnownOauthAuthorizationServerRoute:
|
||||
DotwellKnownOauthAuthorizationServerRoute,
|
||||
DotwellKnownOauthProtectedResourceMcpRoute:
|
||||
DotwellKnownOauthProtectedResourceMcpRoute,
|
||||
ApiAuthSplatRoute: ApiAuthSplatRoute,
|
||||
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) {
|
||||
void navigate({
|
||||
to: "/sign-in",
|
||||
search: { redirect: window.location.pathname },
|
||||
search: {
|
||||
redirect: `${window.location.pathname}${window.location.search}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [isPending, isHostedMode, session?.user?.id, navigate]);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user