From 88eb989547286c8455d4136cf1289f29614808fd Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Wed, 18 Sep 2024 14:00:04 +0200 Subject: [PATCH 01/11] wip --- app/auth/confirm/route.ts | 24 ++------------ middleware.ts | 13 ++++++-- package.json | 1 + tsconfig.json | 2 +- utils/supabase/client.ts | 8 ++--- utils/supabase/middleware.ts | 63 ------------------------------------ utils/supabase/server.ts | 30 ++--------------- 7 files changed, 19 insertions(+), 122 deletions(-) delete mode 100644 utils/supabase/middleware.ts diff --git a/app/auth/confirm/route.ts b/app/auth/confirm/route.ts index d341d85e4..d59093ee5 100644 --- a/app/auth/confirm/route.ts +++ b/app/auth/confirm/route.ts @@ -1,23 +1,3 @@ -import type { EmailOtpType } from '@supabase/supabase-js' -import { createClient } from '@/utils/supabase/server' -import { redirect } from 'next/navigation' +import { createConfirmRoute } from '@supabase/auth/next' -export async function GET(request: Request) { - const { searchParams } = new URL(request.url) - const token_hash = searchParams.get('token_hash') - const type = searchParams.get('type') as EmailOtpType | null - const next = searchParams.get('next') ?? '/' - - if (token_hash && type) { - const supabase = createClient() - - const { error } = await supabase.auth.verifyOtp({ - type, - token_hash - }) - if (!error) { - // redirect user to specified redirect URL or root of app - redirect(next) - } - } -} +export const GET = createConfirmRoute() diff --git a/middleware.ts b/middleware.ts index 5eaa64eea..2dc98cde6 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,8 +1,17 @@ -import { updateSession } from '@/utils/supabase/middleware' +import { createMiddleware } from '@supabase/auth/next' import { NextRequest } from 'next/server' +const supabaseAuthMiddleware = createMiddleware({ + isPublicPath: pathname => + pathname.startsWith('/login') || + pathname.startsWith('/signup') || + pathname.startsWith('/auth'), + loginPath: '/login' +}) + export async function middleware(request: NextRequest) { - return await updateSession(request) + // @ts-expect-error type of request is incompatible because of npm link I guess + return await supabaseAuthMiddleware(request) } export const config = { diff --git a/package.json b/package.json index 9027bb589..bffe1a1dc 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.2", + "@supabase/auth": "^0.1.0", "@supabase/ssr": "^0.5.1", "ai": "^3.3.17", "class-variance-authority": "^0.7.0", diff --git a/tsconfig.json b/tsconfig.json index 9f91b9bae..a1fa76b97 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "incremental": true, "esModuleInterop": true, "module": "esnext", - "moduleResolution": "node", + "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", diff --git a/utils/supabase/client.ts b/utils/supabase/client.ts index 97644e113..017a55f77 100644 --- a/utils/supabase/client.ts +++ b/utils/supabase/client.ts @@ -1,8 +1,4 @@ import { Database } from '@/lib/types' -import { createBrowserClient } from '@supabase/ssr' +import { createClient as createSupabaseClient } from '@supabase/auth/next/client' -export const createClient = () => - createBrowserClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! - ) +export const createClient = () => createSupabaseClient() diff --git a/utils/supabase/middleware.ts b/utils/supabase/middleware.ts deleted file mode 100644 index 40d97e69e..000000000 --- a/utils/supabase/middleware.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { createServerClient } from '@supabase/ssr' -import { type NextRequest, NextResponse } from 'next/server' - -export const updateSession = async (request: NextRequest) => { - // This `try/catch` block is only here for the interactive tutorial. - // Feel free to remove once you have Supabase connected. - try { - // Create an unmodified response - let supabaseResponse = NextResponse.next({ - request - }) - - const supabase = createServerClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, - { - cookies: { - getAll() { - return request.cookies.getAll() - }, - setAll(cookiesToSet) { - cookiesToSet.forEach(({ name, value }) => - request.cookies.set(name, value) - ) - supabaseResponse = NextResponse.next({ - request - }) - cookiesToSet.forEach(({ name, value, options }) => - supabaseResponse.cookies.set(name, value, options) - ) - } - } - } - ) - - const { - data: { user } - } = await supabase.auth.getUser() - - if ( - !user && - !request.nextUrl.pathname.startsWith('/login') && - !request.nextUrl.pathname.startsWith('/signup') && - !request.nextUrl.pathname.startsWith('/auth') - ) { - // no user, potentially respond by redirecting the user to the login page - const url = request.nextUrl.clone() - url.pathname = '/login' - return NextResponse.redirect(url) - } - - return supabaseResponse - } catch (e) { - // If you are here, a Supabase client could not be created! - // This is likely because you have not set up environment variables. - // Check out http://localhost:3000 for Next Steps. - return NextResponse.next({ - request: { - headers: request.headers - } - }) - } -} diff --git a/utils/supabase/server.ts b/utils/supabase/server.ts index 126ca9843..3e30f965f 100644 --- a/utils/supabase/server.ts +++ b/utils/supabase/server.ts @@ -1,30 +1,4 @@ import { Database } from '@/lib/types' -import { createServerClient } from '@supabase/ssr' -import { cookies } from 'next/headers' +import { createClient as createSupabaseClient } from '@supabase/auth/next/server' -export const createClient = () => { - const cookieStore = cookies() - - return createServerClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, - { - cookies: { - getAll() { - return cookieStore.getAll() - }, - setAll(cookiesToSet) { - try { - cookiesToSet.forEach(({ name, value, options }) => { - cookieStore.set(name, value, options) - }) - } catch (error) { - // The `set` method was called from a Server Component. - // This can be ignored if you have middleware refreshing - // user sessions. - } - } - } - } - ) -} +export const createClient = () => createSupabaseClient() From b0ed408a50c80789a5327429e6a9fffa43a643df Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Wed, 18 Sep 2024 14:31:09 +0200 Subject: [PATCH 02/11] install from packed --- middleware.ts | 1 - package-lock.json | 105 +++++++++++++--------------------------------- package.json | 2 +- 3 files changed, 29 insertions(+), 79 deletions(-) diff --git a/middleware.ts b/middleware.ts index 2dc98cde6..2e5ca7f58 100644 --- a/middleware.ts +++ b/middleware.ts @@ -10,7 +10,6 @@ const supabaseAuthMiddleware = createMiddleware({ }) export async function middleware(request: NextRequest) { - // @ts-expect-error type of request is incompatible because of npm link I guess return await supabaseAuthMiddleware(request) } diff --git a/package-lock.json b/package-lock.json index 153daa3eb..d182db115 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,9 +16,8 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.2", + "@supabase/auth": "file:../supabase-auth/supabase-auth-0.1.0.tgz", "@supabase/ssr": "^0.5.1", - "@vercel/analytics": "^1.3.1", - "@vercel/kv": "^2.0.0", "ai": "^3.3.17", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -30,7 +29,6 @@ "openai": "^4.56.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-intersection-observer": "^9.10.3", "react-markdown": "^8.0.7", "react-syntax-highlighter": "^15.5.0", "react-textarea-autosize": "^8.5.3", @@ -40,6 +38,7 @@ "zod": "^3.23.8" }, "devDependencies": { + "@supabase/supabase-js": "^2.45.4", "@tailwindcss/typography": "^0.5.14", "@types/node": "^22.5.0", "@types/react": "^18.3.4", @@ -1610,11 +1609,29 @@ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==" }, + "node_modules/@supabase/auth": { + "version": "0.1.0", + "resolved": "file:../supabase-auth/supabase-auth-0.1.0.tgz", + "integrity": "sha512-7M+vMejJ6Nfce5u3pgeXhtpXETSSyKfENeKJRNjTi6r6fiXdiMzxfdpdcTvGarPSziuZo1591M0Vl9AeC3AL8w==", + "license": "MIT", + "dependencies": { + "defu": "^6.1.4" + }, + "peerDependencies": { + "@supabase/ssr": "^0.5.1", + "@supabase/supabase-js": "^2.45.4", + "next": "^14.0.0-0 || ^15.0.0-0" + }, + "peerDependenciesMeta": { + "next": { + "optional": true + } + } + }, "node_modules/@supabase/auth-js": { "version": "2.65.0", "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.65.0.tgz", "integrity": "sha512-+wboHfZufAE2Y612OsKeVP4rVOeGZzzMLD/Ac3HrTQkkY4qXNjI6Af9gtmxwccE5nFvTiF114FEbIQ1hRq5uUw==", - "peer": true, "dependencies": { "@supabase/node-fetch": "^2.6.14" } @@ -1623,7 +1640,6 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.1.tgz", "integrity": "sha512-8sZ2ibwHlf+WkHDUZJUXqqmPvWQ3UHN0W30behOJngVh/qHHekhJLCFbh0AjkE9/FqqXtf9eoVvmYgfCLk5tNA==", - "peer": true, "dependencies": { "@supabase/node-fetch": "^2.6.14" } @@ -1632,7 +1648,6 @@ "version": "2.6.15", "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", - "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -1644,7 +1659,6 @@ "version": "1.16.1", "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.16.1.tgz", "integrity": "sha512-EOSEZFm5pPuCPGCmLF1VOCS78DfkSz600PBuvBND/IZmMciJ1pmsS3ss6TkB6UkuvTybYiBh7gKOYyxoEO3USA==", - "peer": true, "dependencies": { "@supabase/node-fetch": "^2.6.14" } @@ -1653,7 +1667,6 @@ "version": "2.10.2", "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.10.2.tgz", "integrity": "sha512-qyCQaNg90HmJstsvr2aJNxK2zgoKh9ZZA8oqb7UT2LCh3mj9zpa3Iwu167AuyNxsxrUE8eEJ2yH6wLCij4EApA==", - "peer": true, "dependencies": { "@supabase/node-fetch": "^2.6.14", "@types/phoenix": "^1.5.4", @@ -1676,7 +1689,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.0.tgz", "integrity": "sha512-iZenEdO6Mx9iTR6T7wC7sk6KKsoDPLq8rdu5VRy7+JiT1i8fnqfcOr6mfF2Eaqky9VQzhP8zZKQYjzozB65Rig==", - "peer": true, "dependencies": { "@supabase/node-fetch": "^2.6.14" } @@ -1685,7 +1697,6 @@ "version": "2.45.4", "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.45.4.tgz", "integrity": "sha512-E5p8/zOLaQ3a462MZnmnz03CrduA5ySH9hZyL03Y+QZLIOO4/Gs8Rdy4ZCKDHsN7x0xdanVEWWFN3pJFQr9/hg==", - "peer": true, "dependencies": { "@supabase/auth-js": "2.65.0", "@supabase/functions-js": "2.4.1", @@ -1789,8 +1800,7 @@ "node_modules/@types/phoenix": { "version": "1.6.5", "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.5.tgz", - "integrity": "sha512-xegpDuR+z0UqG9fwHqNoy3rI7JDlvaPh2TY47Fl80oq6g+hXT+c/LEuE43X48clZ6lOfANl5WrPur9fYO1RJ/w==", - "peer": true + "integrity": "sha512-xegpDuR+z0UqG9fwHqNoy3rI7JDlvaPh2TY47Fl80oq6g+hXT+c/LEuE43X48clZ6lOfANl5WrPur9fYO1RJ/w==" }, "node_modules/@types/prop-types": { "version": "15.7.13", @@ -1838,50 +1848,10 @@ "version": "8.5.12", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", - "peer": true, "dependencies": { "@types/node": "*" } }, - "node_modules/@upstash/redis": { - "version": "1.34.0", - "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.34.0.tgz", - "integrity": "sha512-TrXNoJLkysIl8SBc4u9bNnyoFYoILpCcFJcLyWCccb/QSUmaVKdvY0m5diZqc3btExsapcMbaw/s/wh9Sf1pJw==", - "dependencies": { - "crypto-js": "^4.2.0" - } - }, - "node_modules/@vercel/analytics": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.3.1.tgz", - "integrity": "sha512-xhSlYgAuJ6Q4WQGkzYTLmXwhYl39sWjoMA3nHxfkvG+WdBT25c563a7QhwwKivEOZtPJXifYHR1m2ihoisbWyA==", - "dependencies": { - "server-only": "^0.0.1" - }, - "peerDependencies": { - "next": ">= 13", - "react": "^18 || ^19" - }, - "peerDependenciesMeta": { - "next": { - "optional": true - }, - "react": { - "optional": true - } - } - }, - "node_modules/@vercel/kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@vercel/kv/-/kv-2.0.0.tgz", - "integrity": "sha512-zdVrhbzZBYo5d1Hfn4bKtqCeKf0FuzW8rSHauzQVMUgv1+1JOwof2mWcBuI+YMJy8s0G0oqAUfQ7HgUDzb8EbA==", - "dependencies": { - "@upstash/redis": "^1.31.3" - }, - "engines": { - "node": ">=14.6" - } - }, "node_modules/@vue/compiler-core": { "version": "3.5.6", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.6.tgz", @@ -2624,11 +2594,6 @@ "node": ">= 8" } }, - "node_modules/crypto-js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", - "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" - }, "node_modules/css-tree": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", @@ -2721,6 +2686,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -5186,20 +5157,6 @@ "react": "^18.3.1" } }, - "node_modules/react-intersection-observer": { - "version": "9.13.1", - "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.13.1.tgz", - "integrity": "sha512-tSzDaTy0qwNPLJHg8XZhlyHTgGW6drFKTtvjdL+p6um12rcnp8Z5XstE+QNBJ7c64n5o0Lj4ilUleA41bmDoMw==", - "peerDependencies": { - "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -5538,11 +5495,6 @@ "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" }, - "node_modules/server-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", - "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==" - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -6525,7 +6477,6 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index bffe1a1dc..f41200840 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.2", - "@supabase/auth": "^0.1.0", + "@supabase/auth": "file:../supabase-auth/supabase-auth-0.1.0.tgz", "@supabase/ssr": "^0.5.1", "ai": "^3.3.17", "class-variance-authority": "^0.7.0", From 57bbf159ad3bc60d7ea7a6a2d40f3e5ef2493a51 Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Wed, 18 Sep 2024 15:08:41 +0200 Subject: [PATCH 03/11] fix export --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index d182db115..bc41cee0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1612,7 +1612,7 @@ "node_modules/@supabase/auth": { "version": "0.1.0", "resolved": "file:../supabase-auth/supabase-auth-0.1.0.tgz", - "integrity": "sha512-7M+vMejJ6Nfce5u3pgeXhtpXETSSyKfENeKJRNjTi6r6fiXdiMzxfdpdcTvGarPSziuZo1591M0Vl9AeC3AL8w==", + "integrity": "sha512-FglHOw8N7hnu6KD5JOZqlexmCBAhpWLuxIrEZoj9SbxZJ1osUoMoYWCE5oYeHFo7PM3kXbcoeKLdLN73HW51wQ==", "license": "MIT", "dependencies": { "defu": "^6.1.4" From f1666550d837663200d216490f6af0f73aadc273 Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Wed, 18 Sep 2024 18:25:21 +0200 Subject: [PATCH 04/11] wow --- app/auth/confirm/route.ts | 3 -- app/login/page.tsx | 9 ----- app/signup/page.tsx | 9 ----- components/ui/codeblock.tsx | 2 +- components/user-menu.tsx | 2 +- middleware.ts | 34 ++++++++++++------- package-lock.json | 50 +++++++++++++++++----------- package.json | 2 +- supabase/templates/confirmation.html | 2 +- utils/supabase/client.ts | 2 +- utils/supabase/server.ts | 2 +- 11 files changed, 58 insertions(+), 59 deletions(-) delete mode 100644 app/auth/confirm/route.ts diff --git a/app/auth/confirm/route.ts b/app/auth/confirm/route.ts deleted file mode 100644 index d59093ee5..000000000 --- a/app/auth/confirm/route.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { createConfirmRoute } from '@supabase/auth/next' - -export const GET = createConfirmRoute() diff --git a/app/login/page.tsx b/app/login/page.tsx index 1fba27bea..2dd7ac2c8 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,15 +1,6 @@ -import { auth } from '@/auth' import LoginForm from '@/components/login-form' -import { Session } from '@/lib/types' -import { redirect } from 'next/navigation' export default async function LoginPage() { - const session = (await auth()) as Session - - if (session) { - redirect('/') - } - return (
diff --git a/app/signup/page.tsx b/app/signup/page.tsx index dbac96428..1b33b3ca6 100644 --- a/app/signup/page.tsx +++ b/app/signup/page.tsx @@ -1,15 +1,6 @@ -import { auth } from '@/auth' import SignupForm from '@/components/signup-form' -import { Session } from '@/lib/types' -import { redirect } from 'next/navigation' export default async function SignupPage() { - const session = (await auth()) as Session - - if (session) { - redirect('/') - } - return (
diff --git a/components/ui/codeblock.tsx b/components/ui/codeblock.tsx index 1bde5acb5..21fd76ea8 100644 --- a/components/ui/codeblock.tsx +++ b/components/ui/codeblock.tsx @@ -68,7 +68,7 @@ const CodeBlock: FC = memo(({ language, value }) => { 3, true )}${fileExtension}` - const fileName = window.prompt('Enter file name' || '', suggestedFileName) + const fileName = window.prompt('Enter file name', suggestedFileName) if (!fileName) { // User pressed cancel on prompt. diff --git a/components/user-menu.tsx b/components/user-menu.tsx index 42c42494f..e942aeab0 100644 --- a/components/user-menu.tsx +++ b/components/user-menu.tsx @@ -42,7 +42,7 @@ export function UserMenu({ user }: UserMenuProps) { 'use server' const supabase = createClient() await supabase.auth.signOut() - redirect('/login') + redirect('/') }} >