r9stack 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +217 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +239 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/payload/assets/.gitkeep +0 -0
- package/dist/payload/assets/favicon.ico +0 -0
- package/dist/payload/assets/images/r9stack-logo-markonly-circle.png +0 -0
- package/dist/payload/assets/images/r9stack-logo-markonly-whitebg.png +0 -0
- package/dist/payload/assets/images/r9stack-logo-markonly.png +0 -0
- package/dist/payload/assets/images/r9stack-logo.png +0 -0
- package/dist/payload/assets/logo192.png +0 -0
- package/dist/payload/assets/logo512.png +0 -0
- package/dist/payload/assets/manifest.json +26 -0
- package/dist/payload/assets/robots.txt +3 -0
- package/dist/payload/templates/.gitkeep +0 -0
- package/dist/payload/templates/config/components.json +25 -0
- package/dist/payload/templates/config/env.example +14 -0
- package/dist/payload/templates/config/tsconfig.json +26 -0
- package/dist/payload/templates/config/vite.config.ts +23 -0
- package/dist/payload/templates/convex/auth.config.ts +7 -0
- package/dist/payload/templates/convex/messages.ts +28 -0
- package/dist/payload/templates/convex/schema.ts +24 -0
- package/dist/payload/templates/convex/tsconfig.json +21 -0
- package/dist/payload/templates/src/components/AppShell.tsx +21 -0
- package/dist/payload/templates/src/components/AuthProvider.tsx +50 -0
- package/dist/payload/templates/src/components/ConvexClientProvider.tsx +20 -0
- package/dist/payload/templates/src/components/NavGroup.tsx +46 -0
- package/dist/payload/templates/src/components/NavItem.tsx +36 -0
- package/dist/payload/templates/src/components/Sidebar.tsx +76 -0
- package/dist/payload/templates/src/components/UserMenu.tsx +102 -0
- package/dist/payload/templates/src/components/ui/button.tsx +59 -0
- package/dist/payload/templates/src/lib/auth-client.ts +29 -0
- package/dist/payload/templates/src/lib/auth-server.ts +97 -0
- package/dist/payload/templates/src/lib/auth.ts +15 -0
- package/dist/payload/templates/src/lib/utils.ts +7 -0
- package/dist/payload/templates/src/router.tsx +18 -0
- package/dist/payload/templates/src/routes/__root.tsx +53 -0
- package/dist/payload/templates/src/routes/app/demo/convex.messages.tsx +66 -0
- package/dist/payload/templates/src/routes/app/index.tsx +20 -0
- package/dist/payload/templates/src/routes/app/route.tsx +23 -0
- package/dist/payload/templates/src/routes/auth/callback.tsx +36 -0
- package/dist/payload/templates/src/routes/auth/sign-in.tsx +22 -0
- package/dist/payload/templates/src/routes/auth/sign-out.tsx +22 -0
- package/dist/payload/templates/src/routes/index.tsx +85 -0
- package/dist/payload/templates/src/styles.css +141 -0
- package/dist/utils/exec.d.ts +17 -0
- package/dist/utils/exec.d.ts.map +1 -0
- package/dist/utils/exec.js +49 -0
- package/dist/utils/exec.js.map +1 -0
- package/dist/utils/flight-rules.d.ts +5 -0
- package/dist/utils/flight-rules.d.ts.map +1 -0
- package/dist/utils/flight-rules.js +23 -0
- package/dist/utils/flight-rules.js.map +1 -0
- package/dist/utils/github.d.ts +17 -0
- package/dist/utils/github.d.ts.map +1 -0
- package/dist/utils/github.js +64 -0
- package/dist/utils/github.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +27 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/starters.d.ts +20 -0
- package/dist/utils/starters.d.ts.map +1 -0
- package/dist/utils/starters.js +43 -0
- package/dist/utils/starters.js.map +1 -0
- package/dist/utils/templates.d.ts +12 -0
- package/dist/utils/templates.d.ts.map +1 -0
- package/dist/utils/templates.js +77 -0
- package/dist/utils/templates.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default:
|
|
13
|
+
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
|
14
|
+
destructive:
|
|
15
|
+
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
16
|
+
outline:
|
|
17
|
+
"border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
|
|
18
|
+
secondary:
|
|
19
|
+
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
|
20
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
21
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
25
|
+
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
26
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
27
|
+
icon: "size-9",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: {
|
|
31
|
+
variant: "default",
|
|
32
|
+
size: "default",
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
function Button({
|
|
38
|
+
className,
|
|
39
|
+
variant,
|
|
40
|
+
size,
|
|
41
|
+
asChild = false,
|
|
42
|
+
...props
|
|
43
|
+
}: React.ComponentProps<"button"> &
|
|
44
|
+
VariantProps<typeof buttonVariants> & {
|
|
45
|
+
asChild?: boolean
|
|
46
|
+
}) {
|
|
47
|
+
const Comp = asChild ? Slot : "button"
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Comp
|
|
51
|
+
data-slot="button"
|
|
52
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { Button, buttonVariants }
|
|
59
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react'
|
|
2
|
+
import type { User } from './auth'
|
|
3
|
+
|
|
4
|
+
export interface AuthContextValue {
|
|
5
|
+
user: User | null
|
|
6
|
+
isAuthenticated: boolean
|
|
7
|
+
isLoading: boolean
|
|
8
|
+
signIn: () => void
|
|
9
|
+
signOut: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const AuthContext = createContext<AuthContextValue | null>(null)
|
|
13
|
+
|
|
14
|
+
export function useAuth(): AuthContextValue {
|
|
15
|
+
const context = useContext(AuthContext)
|
|
16
|
+
if (!context) {
|
|
17
|
+
throw new Error('useAuth must be used within an AuthProvider')
|
|
18
|
+
}
|
|
19
|
+
return context
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function signIn() {
|
|
23
|
+
window.location.href = '/auth/sign-in'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function signOut() {
|
|
27
|
+
window.location.href = '/auth/sign-out'
|
|
28
|
+
}
|
|
29
|
+
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
2
|
+
import { getRequest, setCookie, deleteCookie } from '@tanstack/react-start/server'
|
|
3
|
+
import { getIronSession } from 'iron-session'
|
|
4
|
+
import { WorkOS } from '@workos-inc/node'
|
|
5
|
+
import type { SessionData, User } from './auth'
|
|
6
|
+
|
|
7
|
+
const workos = new WorkOS(process.env.WORKOS_API_KEY)
|
|
8
|
+
const clientId = process.env.WORKOS_CLIENT_ID!
|
|
9
|
+
|
|
10
|
+
const sessionOptions = {
|
|
11
|
+
password: process.env.WORKOS_COOKIE_PASSWORD!,
|
|
12
|
+
cookieName: 'r9_session',
|
|
13
|
+
cookieOptions: {
|
|
14
|
+
secure: process.env.NODE_ENV === 'production',
|
|
15
|
+
httpOnly: true,
|
|
16
|
+
sameSite: 'lax' as const,
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const getAuthUrl = createServerFn({ method: 'GET' }).handler(async () => {
|
|
21
|
+
const authorizationUrl = workos.userManagement.getAuthorizationUrl({
|
|
22
|
+
provider: 'authkit',
|
|
23
|
+
clientId,
|
|
24
|
+
redirectUri: process.env.WORKOS_REDIRECT_URI!,
|
|
25
|
+
})
|
|
26
|
+
return authorizationUrl
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export const getCurrentUser = createServerFn({ method: 'GET' }).handler(
|
|
30
|
+
async (): Promise<User | null> => {
|
|
31
|
+
const request = getRequest()
|
|
32
|
+
if (!request) return null
|
|
33
|
+
|
|
34
|
+
const response = new Response()
|
|
35
|
+
const session = await getIronSession<SessionData>(request, response, sessionOptions)
|
|
36
|
+
|
|
37
|
+
if (!session.user) return null
|
|
38
|
+
if (session.expiresAt && Date.now() > session.expiresAt) return null
|
|
39
|
+
|
|
40
|
+
return session.user
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
export const handleAuthCallback = createServerFn({ method: 'GET' }).handler(
|
|
45
|
+
async (ctx: { data: { code: string } }): Promise<User> => {
|
|
46
|
+
const { code } = ctx.data
|
|
47
|
+
|
|
48
|
+
const authResponse = await workos.userManagement.authenticateWithCode({
|
|
49
|
+
clientId,
|
|
50
|
+
code,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const user: User = {
|
|
54
|
+
id: authResponse.user.id,
|
|
55
|
+
email: authResponse.user.email,
|
|
56
|
+
firstName: authResponse.user.firstName,
|
|
57
|
+
lastName: authResponse.user.lastName,
|
|
58
|
+
profilePictureUrl: authResponse.user.profilePictureUrl,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const request = getRequest()
|
|
62
|
+
if (!request) throw new Error('No request available')
|
|
63
|
+
|
|
64
|
+
const response = new Response()
|
|
65
|
+
const session = await getIronSession<SessionData>(request, response, sessionOptions)
|
|
66
|
+
|
|
67
|
+
session.user = user
|
|
68
|
+
session.accessToken = authResponse.accessToken
|
|
69
|
+
session.refreshToken = authResponse.refreshToken
|
|
70
|
+
session.expiresAt = Date.now() + 60 * 60 * 1000 // 1 hour
|
|
71
|
+
|
|
72
|
+
await session.save()
|
|
73
|
+
|
|
74
|
+
const cookieHeader = response.headers.get('Set-Cookie')
|
|
75
|
+
if (cookieHeader) {
|
|
76
|
+
const [nameValue] = cookieHeader.split(';')
|
|
77
|
+
const [, value] = nameValue.split('=')
|
|
78
|
+
setCookie('r9_session', value, {
|
|
79
|
+
httpOnly: true,
|
|
80
|
+
secure: process.env.NODE_ENV === 'production',
|
|
81
|
+
sameSite: 'lax',
|
|
82
|
+
maxAge: 60 * 60,
|
|
83
|
+
path: '/',
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return user
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
export const signOutServer = createServerFn({ method: 'POST' }).handler(
|
|
92
|
+
async () => {
|
|
93
|
+
deleteCookie('r9_session')
|
|
94
|
+
return { success: true }
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface User {
|
|
2
|
+
id: string
|
|
3
|
+
email: string
|
|
4
|
+
firstName: string | null
|
|
5
|
+
lastName: string | null
|
|
6
|
+
profilePictureUrl: string | null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SessionData {
|
|
10
|
+
user?: User
|
|
11
|
+
accessToken?: string
|
|
12
|
+
refreshToken?: string
|
|
13
|
+
expiresAt?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
|
|
2
|
+
import { routeTree } from './routeTree.gen'
|
|
3
|
+
|
|
4
|
+
export function createRouter() {
|
|
5
|
+
const router = createTanStackRouter({
|
|
6
|
+
routeTree,
|
|
7
|
+
scrollRestoration: true,
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
return router
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
declare module '@tanstack/react-router' {
|
|
14
|
+
interface Register {
|
|
15
|
+
router: ReturnType<typeof createRouter>
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { HeadContent, Scripts, createRootRoute, Outlet } from '@tanstack/react-router'
|
|
2
|
+
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
|
|
3
|
+
import { TanStackDevtools } from '@tanstack/react-devtools'
|
|
4
|
+
|
|
5
|
+
import { AuthProvider } from '../components/AuthProvider'
|
|
6
|
+
import { ConvexClientProvider } from '../components/ConvexClientProvider'
|
|
7
|
+
|
|
8
|
+
import appCss from '../styles.css?url'
|
|
9
|
+
|
|
10
|
+
export const Route = createRootRoute({
|
|
11
|
+
head: () => ({
|
|
12
|
+
meta: [
|
|
13
|
+
{ charSet: 'utf-8' },
|
|
14
|
+
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
|
15
|
+
{ title: '{{PROJECT_NAME}}' },
|
|
16
|
+
{ property: 'og:type', content: 'website' },
|
|
17
|
+
{ property: 'og:title', content: '{{PROJECT_NAME}}' },
|
|
18
|
+
{ property: 'og:description', content: 'Built with r9stack' },
|
|
19
|
+
{ property: 'og:image', content: '/images/logo.png' },
|
|
20
|
+
{ name: 'twitter:card', content: 'summary' },
|
|
21
|
+
{ name: 'twitter:title', content: '{{PROJECT_NAME}}' },
|
|
22
|
+
{ name: 'twitter:description', content: 'Built with r9stack' },
|
|
23
|
+
{ name: 'twitter:image', content: '/images/logo.png' },
|
|
24
|
+
],
|
|
25
|
+
links: [{ rel: 'stylesheet', href: appCss }],
|
|
26
|
+
}),
|
|
27
|
+
shellComponent: RootDocument,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
function RootDocument({ children }: { children: React.ReactNode }) {
|
|
31
|
+
return (
|
|
32
|
+
<html lang="en">
|
|
33
|
+
<head>
|
|
34
|
+
<HeadContent />
|
|
35
|
+
</head>
|
|
36
|
+
<body>
|
|
37
|
+
<AuthProvider>
|
|
38
|
+
<ConvexClientProvider>
|
|
39
|
+
{children}
|
|
40
|
+
</ConvexClientProvider>
|
|
41
|
+
</AuthProvider>
|
|
42
|
+
<TanStackDevtools
|
|
43
|
+
config={{ position: 'bottom-right' }}
|
|
44
|
+
plugins={[
|
|
45
|
+
{ name: 'Tanstack Router', render: <TanStackRouterDevtoolsPanel /> },
|
|
46
|
+
]}
|
|
47
|
+
/>
|
|
48
|
+
<Scripts />
|
|
49
|
+
</body>
|
|
50
|
+
</html>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { useQuery, useMutation } from 'convex/react'
|
|
3
|
+
import { api } from '../../../../convex/_generated/api'
|
|
4
|
+
import { useState } from 'react'
|
|
5
|
+
import { Button } from '../../../components/ui/button'
|
|
6
|
+
|
|
7
|
+
export const Route = createFileRoute('/app/demo/convex/messages')({
|
|
8
|
+
component: MessagesDemo,
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
function MessagesDemo() {
|
|
12
|
+
const messages = useQuery(api.messages.list)
|
|
13
|
+
const sendMessage = useMutation(api.messages.send)
|
|
14
|
+
const [newMessage, setNewMessage] = useState('')
|
|
15
|
+
|
|
16
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
17
|
+
e.preventDefault()
|
|
18
|
+
if (!newMessage.trim()) return
|
|
19
|
+
|
|
20
|
+
await sendMessage({ text: newMessage.trim() })
|
|
21
|
+
setNewMessage('')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="p-8 max-w-2xl">
|
|
26
|
+
<h1 className="text-2xl font-bold text-foreground mb-2">
|
|
27
|
+
Convex Messages Demo
|
|
28
|
+
</h1>
|
|
29
|
+
<p className="text-muted-foreground mb-6">
|
|
30
|
+
Real-time messages powered by Convex. Open in multiple tabs to see sync!
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
<form onSubmit={handleSubmit} className="flex gap-2 mb-6">
|
|
34
|
+
<input
|
|
35
|
+
type="text"
|
|
36
|
+
value={newMessage}
|
|
37
|
+
onChange={(e) => setNewMessage(e.target.value)}
|
|
38
|
+
placeholder="Type a message..."
|
|
39
|
+
className="flex-1 px-3 py-2 border border-input rounded-md bg-background text-foreground"
|
|
40
|
+
/>
|
|
41
|
+
<Button type="submit">Send</Button>
|
|
42
|
+
</form>
|
|
43
|
+
|
|
44
|
+
<div className="space-y-2">
|
|
45
|
+
{messages === undefined ? (
|
|
46
|
+
<p className="text-muted-foreground">Loading...</p>
|
|
47
|
+
) : messages.length === 0 ? (
|
|
48
|
+
<p className="text-muted-foreground">No messages yet. Send one!</p>
|
|
49
|
+
) : (
|
|
50
|
+
messages.map((message) => (
|
|
51
|
+
<div
|
|
52
|
+
key={message._id}
|
|
53
|
+
className="p-3 bg-muted rounded-md"
|
|
54
|
+
>
|
|
55
|
+
<p className="text-foreground">{message.text}</p>
|
|
56
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
57
|
+
{new Date(message.createdAt).toLocaleString()}
|
|
58
|
+
</p>
|
|
59
|
+
</div>
|
|
60
|
+
))
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { useAuth } from '../../lib/auth-client'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/app/')({ component: AppHome })
|
|
5
|
+
|
|
6
|
+
function AppHome() {
|
|
7
|
+
const { user } = useAuth()
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div className="p-8">
|
|
11
|
+
<h1 className="text-3xl font-bold text-foreground mb-4">
|
|
12
|
+
Welcome{user?.firstName ? `, ${user.firstName}` : ''}!
|
|
13
|
+
</h1>
|
|
14
|
+
<p className="text-muted-foreground">
|
|
15
|
+
You're now in the authenticated area of your app.
|
|
16
|
+
</p>
|
|
17
|
+
</div>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
|
|
2
|
+
import { getCurrentUser } from '../../lib/auth-server'
|
|
3
|
+
import { AppShell } from '../../components/AppShell'
|
|
4
|
+
|
|
5
|
+
export const Route = createFileRoute('/app')({
|
|
6
|
+
beforeLoad: async () => {
|
|
7
|
+
const user = await getCurrentUser()
|
|
8
|
+
if (!user) {
|
|
9
|
+
throw redirect({ to: '/' })
|
|
10
|
+
}
|
|
11
|
+
return {}
|
|
12
|
+
},
|
|
13
|
+
component: AppLayout,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
function AppLayout() {
|
|
17
|
+
return (
|
|
18
|
+
<AppShell>
|
|
19
|
+
<Outlet />
|
|
20
|
+
</AppShell>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createFileRoute, redirect } from '@tanstack/react-router'
|
|
2
|
+
import { handleAuthCallback } from '../../lib/auth-server'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/auth/callback')({
|
|
5
|
+
beforeLoad: async ({ search }) => {
|
|
6
|
+
const code = (search as { code?: string }).code
|
|
7
|
+
|
|
8
|
+
if (!code) {
|
|
9
|
+
throw redirect({ to: '/', search: { error: 'no_code' } })
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
await handleAuthCallback({ data: { code } })
|
|
14
|
+
throw redirect({ to: '/app' })
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error instanceof Response || (error as { to?: string })?.to) {
|
|
17
|
+
throw error
|
|
18
|
+
}
|
|
19
|
+
console.error('Auth callback error:', error)
|
|
20
|
+
throw redirect({ to: '/', search: { error: 'auth_failed' } })
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
component: CallbackPage,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
function CallbackPage() {
|
|
27
|
+
return (
|
|
28
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
29
|
+
<div className="text-center">
|
|
30
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4" />
|
|
31
|
+
<p className="text-muted-foreground">Completing sign in...</p>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createFileRoute, redirect } from '@tanstack/react-router'
|
|
2
|
+
import { getAuthUrl } from '../../lib/auth-server'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/auth/sign-in')({
|
|
5
|
+
beforeLoad: async () => {
|
|
6
|
+
const authUrl = await getAuthUrl()
|
|
7
|
+
throw redirect({ href: authUrl })
|
|
8
|
+
},
|
|
9
|
+
component: SignInPage,
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
function SignInPage() {
|
|
13
|
+
return (
|
|
14
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
15
|
+
<div className="text-center">
|
|
16
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4" />
|
|
17
|
+
<p className="text-muted-foreground">Redirecting to sign in...</p>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createFileRoute, redirect } from '@tanstack/react-router'
|
|
2
|
+
import { signOutServer } from '../../lib/auth-server'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/auth/sign-out')({
|
|
5
|
+
beforeLoad: async () => {
|
|
6
|
+
await signOutServer()
|
|
7
|
+
throw redirect({ to: '/' })
|
|
8
|
+
},
|
|
9
|
+
component: SignOutPage,
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
function SignOutPage() {
|
|
13
|
+
return (
|
|
14
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
15
|
+
<div className="text-center">
|
|
16
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4" />
|
|
17
|
+
<p className="text-muted-foreground">Signing out...</p>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createFileRoute, Link } from '@tanstack/react-router'
|
|
2
|
+
import { useAuth } from '../lib/auth-client'
|
|
3
|
+
import { Button } from '../components/ui/button'
|
|
4
|
+
|
|
5
|
+
export const Route = createFileRoute('/')({ component: LandingPage })
|
|
6
|
+
|
|
7
|
+
function LandingPage() {
|
|
8
|
+
const { user, isAuthenticated, isLoading, signIn } = useAuth()
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="min-h-screen bg-background">
|
|
12
|
+
{/* Header */}
|
|
13
|
+
<header className="border-b border-border">
|
|
14
|
+
<div className="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
|
|
15
|
+
<div className="flex items-center gap-3">
|
|
16
|
+
<span className="text-xl font-semibold text-foreground">
|
|
17
|
+
{{PROJECT_NAME}}
|
|
18
|
+
</span>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div className="flex items-center gap-3">
|
|
22
|
+
{isLoading ? (
|
|
23
|
+
<div className="w-20 h-9 bg-muted animate-pulse rounded-md" />
|
|
24
|
+
) : isAuthenticated ? (
|
|
25
|
+
<Link to="/app">
|
|
26
|
+
<Button>Go to App</Button>
|
|
27
|
+
</Link>
|
|
28
|
+
) : (
|
|
29
|
+
<>
|
|
30
|
+
<Button variant="ghost" onClick={signIn}>
|
|
31
|
+
Sign In
|
|
32
|
+
</Button>
|
|
33
|
+
<Button onClick={signIn}>Get Started</Button>
|
|
34
|
+
</>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</header>
|
|
39
|
+
|
|
40
|
+
{/* Hero Section */}
|
|
41
|
+
<main className="max-w-6xl mx-auto px-6 py-20">
|
|
42
|
+
<div className="text-center max-w-3xl mx-auto">
|
|
43
|
+
<h1 className="text-5xl font-bold tracking-tight text-foreground mb-6">
|
|
44
|
+
Welcome to {{PROJECT_NAME}}
|
|
45
|
+
</h1>
|
|
46
|
+
<p className="text-xl text-muted-foreground mb-10">
|
|
47
|
+
Built with r9stack — TanStack Start, Convex, WorkOS, and shadcn/ui
|
|
48
|
+
pre-integrated.
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
<div className="flex items-center justify-center gap-4 mb-16">
|
|
52
|
+
{isAuthenticated ? (
|
|
53
|
+
<Link to="/app">
|
|
54
|
+
<Button size="lg" className="px-8">
|
|
55
|
+
Open App
|
|
56
|
+
</Button>
|
|
57
|
+
</Link>
|
|
58
|
+
) : (
|
|
59
|
+
<>
|
|
60
|
+
<Button size="lg" className="px-8" onClick={signIn}>
|
|
61
|
+
Get Started Free
|
|
62
|
+
</Button>
|
|
63
|
+
<Button size="lg" variant="outline" onClick={signIn}>
|
|
64
|
+
Sign In
|
|
65
|
+
</Button>
|
|
66
|
+
</>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{isAuthenticated && user && (
|
|
71
|
+
<div className="mb-16 p-4 bg-muted/50 rounded-lg border border-border inline-block">
|
|
72
|
+
<p className="text-sm text-muted-foreground">
|
|
73
|
+
Signed in as{' '}
|
|
74
|
+
<span className="font-medium text-foreground">
|
|
75
|
+
{user.email}
|
|
76
|
+
</span>
|
|
77
|
+
</p>
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
</main>
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|