diff --git a/.env.example b/.env.example
index 3c7840a9..5c99d5eb 100644
--- a/.env.example
+++ b/.env.example
@@ -1,5 +1,24 @@
# Rename this file to .env once you have filled in the below environment variables!
+# GitHub OAuth Authentication Configuration
+# ---------------------------------------
+# To set up GitHub OAuth:
+# 1. Go to https://github.com/settings/developers
+# 2. Click "New OAuth App"
+# 3. Fill in the application details:
+# - Application name: Buildify (or your preferred name)
+# - Homepage URL: http://localhost:5173 (or your deployment URL)
+# - Authorization callback URL: http://localhost:5173/auth/callback
+# 4. Click "Register application"
+# 5. Copy the Client ID and generate a new Client Secret
+# 6. Paste them below
+GITHUB_CLIENT_ID=
+GITHUB_CLIENT_SECRET=
+
+# Session Secret - Used to encrypt session cookies
+# Generate a random string (e.g., using `openssl rand -hex 32` in terminal)
+SESSION_SECRET=
+
# Get your GROQ API Key here -
# https://console.groq.com/keys
# You only need this environment variable set if you want to use Groq models
diff --git a/app/components/header/Header.tsx b/app/components/header/Header.tsx
index ce46702a..a6158e03 100644
--- a/app/components/header/Header.tsx
+++ b/app/components/header/Header.tsx
@@ -4,10 +4,12 @@ import { chatStore } from '~/lib/stores/chat';
import { classNames } from '~/utils/classNames';
import { HeaderActionButtons } from './HeaderActionButtons.client';
import { ChatDescription } from '~/lib/persistence/ChatDescription.client';
+import { useAuth } from '~/lib/hooks/useAuth';
+import { Link } from '@remix-run/react';
export function Header() {
const chat = useStore(chatStore);
-
+
return (
- {chat.started && ( // Display ChatDescription and HeaderActionButtons only when the chat has started.
- <>
-
- {() => }
-
+
+ {chat.started && (
+
+ {() => }
+
+ )}
+
+
+ {/* Authentication Controls - Always visible */}
+
+ {() => }
+
+
+ {/* Existing Action Buttons - Only when chat has started */}
+ {chat.started && (
{() => (
-
);
}
+
+function AuthControls() {
+ const { isAuthenticated, isLoading, user, login, logout } = useAuth();
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (isAuthenticated && user) {
+ return (
+
+
+
+
+
+ {user.username}
+
+
+
+
+
+
+ Signed in as {user.username}
+
+
+
+ Your profile
+
+
+
logout()}
+ className="block w-full text-left px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive"
+ >
+ Sign out
+
+
+
+
+ );
+ }
+
+ return (
+ login()}
+ className="flex items-center gap-2 px-3 py-1.5 rounded-md bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent hover:bg-bolt-elements-item-backgroundAccentHover transition-colors text-sm font-medium"
+ >
+
+ Sign in
+
+ );
+}
diff --git a/app/lib/auth/github-oauth.server.ts b/app/lib/auth/github-oauth.server.ts
new file mode 100644
index 00000000..e12266b7
--- /dev/null
+++ b/app/lib/auth/github-oauth.server.ts
@@ -0,0 +1,241 @@
+import { OAuthApp } from '@octokit/oauth-app';
+import { createCookieSessionStorage, redirect } from '@remix-run/node';
+import { v4 as uuidv4 } from 'uuid';
+
+// Define types for GitHub user data
+export interface GitHubUser {
+ id: number;
+ login: string;
+ name: string | null;
+ email: string | null;
+ avatar_url: string;
+ html_url: string;
+}
+
+// GitHub API response types
+interface GitHubUserResponse {
+ id: number;
+ login: string;
+ name: string | null;
+ email: string | null;
+ avatar_url: string;
+ html_url: string;
+ [key: string]: any; // For other properties we don't explicitly use
+}
+
+interface GitHubEmailResponse {
+ email: string;
+ primary: boolean;
+ verified: boolean;
+ visibility: string | null;
+}
+
+// Define types for session data
+export interface AuthSession {
+ accessToken: string;
+ user: GitHubUser;
+}
+
+// Error class for authentication errors
+export class AuthError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = 'AuthError';
+ }
+}
+
+// Get environment variables
+const clientId = process.env.GITHUB_CLIENT_ID;
+const clientSecret = process.env.GITHUB_CLIENT_SECRET;
+const sessionSecret = process.env.SESSION_SECRET || 'buildify-session-secret';
+
+// Validate required environment variables
+if (!clientId || !clientSecret) {
+ console.warn(
+ 'GitHub OAuth is not configured properly. Please set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET environment variables.'
+ );
+}
+
+// Create OAuth app instance
+export const oauthApp = new OAuthApp({
+ clientId: clientId || '',
+ clientSecret: clientSecret || '',
+ defaultScopes: ['read:user', 'user:email'],
+});
+
+// Create session storage
+const sessionStorage = createCookieSessionStorage({
+ cookie: {
+ name: 'buildify_auth_session',
+ httpOnly: true,
+ path: '/',
+ sameSite: 'lax',
+ secrets: [sessionSecret],
+ secure: process.env.NODE_ENV === 'production',
+ },
+});
+
+// Get session from request
+export async function getSession(request: Request) {
+ const cookie = request.headers.get('Cookie');
+ return sessionStorage.getSession(cookie);
+}
+
+// Generate state parameter for OAuth flow to prevent CSRF attacks
+export function generateState() {
+ return uuidv4();
+}
+
+// Store state in session
+export async function storeState(request: Request, state: string) {
+ const session = await getSession(request);
+ session.set('oauth:state', state);
+ return sessionStorage.commitSession(session);
+}
+
+// Verify state from session
+export async function verifyState(request: Request, state: string) {
+ const session = await getSession(request);
+ const storedState = session.get('oauth:state');
+
+ if (!storedState || storedState !== state) {
+ throw new AuthError('Invalid state parameter. Possible CSRF attack.');
+ }
+
+ // Clear the state after verification
+ session.unset('oauth:state');
+ return sessionStorage.commitSession(session);
+}
+
+// Generate authorization URL
+export function getAuthorizationUrl(state: string, redirectUri?: string) {
+ return oauthApp.getWebFlowAuthorizationUrl({
+ state,
+ redirectUrl: redirectUri,
+ });
+}
+
+// Exchange code for token
+export async function exchangeCodeForToken(code: string, state: string) {
+ try {
+ const { authentication } = await oauthApp.createToken({
+ code,
+ state,
+ });
+
+ return {
+ accessToken: authentication.token,
+ };
+ } catch (error) {
+ console.error('Error exchanging code for token:', error);
+ throw new AuthError('Failed to exchange code for token');
+ }
+}
+
+// Fetch user data from GitHub API
+export async function fetchGitHubUser(accessToken: string): Promise {
+ try {
+ const response = await fetch('https://api.github.com/user', {
+ headers: {
+ Accept: 'application/vnd.github.v3+json',
+ Authorization: `token ${accessToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`GitHub API responded with ${response.status}`);
+ }
+
+ const userData = await response.json() as GitHubUserResponse;
+
+ // If email is not public, try to get primary email
+ let email = userData.email;
+ if (!email) {
+ const emailsResponse = await fetch('https://api.github.com/user/emails', {
+ headers: {
+ Accept: 'application/vnd.github.v3+json',
+ Authorization: `token ${accessToken}`,
+ },
+ });
+
+ if (emailsResponse.ok) {
+ const emails = await emailsResponse.json() as GitHubEmailResponse[];
+ const primaryEmail = emails.find((e) => e.primary);
+ email = primaryEmail?.email || null;
+ }
+ }
+
+ return {
+ id: userData.id,
+ login: userData.login,
+ name: userData.name,
+ email: email,
+ avatar_url: userData.avatar_url,
+ html_url: userData.html_url,
+ };
+ } catch (error) {
+ console.error('Error fetching GitHub user:', error);
+ throw new AuthError('Failed to fetch user data from GitHub');
+ }
+}
+
+// Create user session
+export async function createUserSession(request: Request, authSession: AuthSession, redirectTo: string) {
+ const session = await getSession(request);
+
+ // Store user data in session
+ session.set('auth:user', authSession.user);
+ session.set('auth:accessToken', authSession.accessToken);
+
+ // Commit session and redirect
+ return redirect(redirectTo, {
+ headers: {
+ 'Set-Cookie': await sessionStorage.commitSession(session, {
+ maxAge: 60 * 60 * 24 * 7, // 1 week
+ }),
+ },
+ });
+}
+
+// Get user from session
+export async function getUserFromSession(request: Request): Promise {
+ const session = await getSession(request);
+ const user = session.get('auth:user');
+ return user || null;
+}
+
+// Get access token from session
+export async function getAccessToken(request: Request): Promise {
+ const session = await getSession(request);
+ const accessToken = session.get('auth:accessToken');
+ return accessToken || null;
+}
+
+// Check if user is authenticated
+export async function isAuthenticated(request: Request): Promise {
+ const user = await getUserFromSession(request);
+ return user !== null;
+}
+
+// Require authentication
+export async function requireAuthentication(request: Request, redirectTo: string = '/login') {
+ const authenticated = await isAuthenticated(request);
+
+ if (!authenticated) {
+ const searchParams = new URLSearchParams([['redirectTo', request.url]]);
+ throw redirect(`${redirectTo}?${searchParams}`);
+ }
+
+ return await getUserFromSession(request);
+}
+
+// Logout - destroy session
+export async function logout(request: Request, redirectTo: string = '/') {
+ const session = await getSession(request);
+
+ return redirect(redirectTo, {
+ headers: {
+ 'Set-Cookie': await sessionStorage.destroySession(session),
+ },
+ });
+}
diff --git a/app/lib/hooks/useAuth.ts b/app/lib/hooks/useAuth.ts
new file mode 100644
index 00000000..f371abbf
--- /dev/null
+++ b/app/lib/hooks/useAuth.ts
@@ -0,0 +1,134 @@
+import { useEffect, useState } from 'react';
+import { useNavigate, useLocation } from '@remix-run/react';
+import {
+ profileStore,
+ setAuthenticatedUser,
+ clearAuthState,
+ isAuthenticated as getIsAuthenticated,
+ getGitHubUser
+} from '~/lib/stores/profile';
+import { useStore } from '@nanostores/react';
+import type { GitHubUser } from '~/lib/auth/github-oauth.server';
+
+// Define the API response type for auth status
+interface AuthStatusResponse {
+ isAuthenticated: boolean;
+ user?: GitHubUser;
+ tokenStatus?: {
+ hasToken: boolean;
+ };
+ error?: string;
+}
+
+/**
+ * Hook to manage authentication state and sync between server and client
+ */
+export function useAuth() {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const profile = useStore(profileStore);
+ const [isLoading, setIsLoading] = useState(true);
+ const [serverChecked, setServerChecked] = useState(false);
+
+ // Sync server authentication state with client on initial load
+ useEffect(() => {
+ const checkServerAuth = async () => {
+ try {
+ setIsLoading(true);
+ // Fetch authentication status from server
+ const response = await fetch('/api/auth/status');
+
+ if (response.ok) {
+ const data = await response.json() as AuthStatusResponse;
+
+ if (data.isAuthenticated && data.user) {
+ // Update client state with server auth data
+ setAuthenticatedUser(data.user);
+ } else if (profile.isAuthenticated) {
+ // Clear client state if server says not authenticated
+ clearAuthState();
+ }
+ } else {
+ // Handle error - assume not authenticated
+ if (profile.isAuthenticated) {
+ clearAuthState();
+ }
+ }
+ } catch (error) {
+ console.error('Error checking authentication status:', error);
+ } finally {
+ setIsLoading(false);
+ setServerChecked(true);
+ }
+ };
+
+ // Only check server auth once
+ if (!serverChecked) {
+ checkServerAuth();
+ }
+ }, [serverChecked, profile.isAuthenticated]);
+
+ /**
+ * Navigate to login page
+ */
+ const login = (redirectTo?: string) => {
+ const searchParams = new URLSearchParams();
+
+ if (redirectTo || location.pathname !== '/login') {
+ searchParams.set('redirectTo', redirectTo || location.pathname);
+ }
+
+ const searchParamsString = searchParams.toString();
+ navigate(`/auth/login${searchParamsString ? `?${searchParamsString}` : ''}`);
+ };
+
+ /**
+ * Navigate to logout page
+ */
+ const logout = (redirectTo?: string) => {
+ const searchParams = new URLSearchParams();
+
+ if (redirectTo) {
+ searchParams.set('redirectTo', redirectTo);
+ }
+
+ const searchParamsString = searchParams.toString();
+ navigate(`/auth/logout${searchParamsString ? `?${searchParamsString}` : ''}`);
+ };
+
+ /**
+ * Check if user needs to authenticate for a protected resource
+ * and redirect to login if needed
+ */
+ const requireAuth = (redirectIfNotAuth: boolean = true) => {
+ if (!isLoading && !profile.isAuthenticated && redirectIfNotAuth) {
+ login();
+ return false;
+ }
+
+ return profile.isAuthenticated;
+ };
+
+ return {
+ // Authentication state
+ isAuthenticated: profile.isAuthenticated,
+ isLoading,
+
+ // User data
+ user: profile.isAuthenticated ? {
+ username: profile.username,
+ avatar: profile.avatar,
+ bio: profile.bio,
+ githubUser: profile.github,
+ lastLogin: profile.lastLogin
+ } : null,
+
+ // GitHub specific data
+ githubUser: getGitHubUser(),
+
+ // Actions
+ login,
+ logout,
+ requireAuth,
+ };
+}
diff --git a/app/lib/stores/profile.ts b/app/lib/stores/profile.ts
index d0243ce6..fa136df2 100644
--- a/app/lib/stores/profile.ts
+++ b/app/lib/stores/profile.ts
@@ -1,23 +1,54 @@
import { atom } from 'nanostores';
+/**
+ * Enhanced Profile interface with authentication-related fields
+ */
interface Profile {
+ // Basic profile fields (original)
username: string;
bio: string;
avatar: string;
+
+ // Authentication state
+ isAuthenticated: boolean;
+ lastLogin?: number; // timestamp
+
+ // GitHub user data
+ github?: {
+ id: number;
+ login: string;
+ name: string | null;
+ email: string | null;
+ avatar_url: string;
+ html_url: string;
+ };
}
// Initialize with stored profile or defaults
const storedProfile = typeof window !== 'undefined' ? localStorage.getItem('bolt_profile') : null;
const initialProfile: Profile = storedProfile
- ? JSON.parse(storedProfile)
+ ? {
+ // Ensure backward compatibility with existing profile data
+ ...{
+ username: '',
+ bio: '',
+ avatar: '',
+ isAuthenticated: false,
+ },
+ ...JSON.parse(storedProfile),
+ }
: {
username: '',
bio: '',
avatar: '',
+ isAuthenticated: false,
};
export const profileStore = atom(initialProfile);
+/**
+ * Update profile with partial data
+ */
export const updateProfile = (updates: Partial) => {
profileStore.set({ ...profileStore.get(), ...updates });
@@ -26,3 +57,57 @@ export const updateProfile = (updates: Partial) => {
localStorage.setItem('bolt_profile', JSON.stringify(profileStore.get()));
}
};
+
+/**
+ * Set authenticated user data from GitHub
+ */
+export const setAuthenticatedUser = (githubUser: {
+ id: number;
+ login: string;
+ name: string | null;
+ email: string | null;
+ avatar_url: string;
+ html_url: string;
+}) => {
+ const now = Date.now();
+
+ updateProfile({
+ username: githubUser.login,
+ avatar: githubUser.avatar_url,
+ // Keep existing bio if available
+ bio: profileStore.get().bio || githubUser.name || '',
+ isAuthenticated: true,
+ lastLogin: now,
+ github: githubUser,
+ });
+};
+
+/**
+ * Check if the user is authenticated
+ */
+export const isAuthenticated = (): boolean => {
+ return profileStore.get().isAuthenticated;
+};
+
+/**
+ * Clear authentication state
+ */
+export const clearAuthState = () => {
+ const currentProfile = profileStore.get();
+
+ updateProfile({
+ // Keep username/bio/avatar if user wants to
+ // but clear authentication state
+ isAuthenticated: false,
+ github: undefined,
+ lastLogin: undefined,
+ });
+};
+
+/**
+ * Get GitHub user data if authenticated
+ */
+export const getGitHubUser = () => {
+ const profile = profileStore.get();
+ return profile.isAuthenticated ? profile.github : null;
+};
diff --git a/app/routes/api.auth.status.ts b/app/routes/api.auth.status.ts
new file mode 100644
index 00000000..6a8a4470
--- /dev/null
+++ b/app/routes/api.auth.status.ts
@@ -0,0 +1,57 @@
+import { json, type LoaderFunctionArgs } from '@remix-run/node';
+import {
+ getUserFromSession,
+ isAuthenticated,
+ getAccessToken,
+} from '~/lib/auth/github-oauth.server';
+
+/**
+ * API route to check authentication status
+ * Used by the client to sync authentication state between server and client
+ */
+export async function loader({ request }: LoaderFunctionArgs) {
+ // Only allow GET requests
+ if (request.method !== 'GET') {
+ return json(
+ { error: 'Method not allowed' },
+ { status: 405, headers: { 'Allow': 'GET' } }
+ );
+ }
+
+ try {
+ // Check if user is authenticated
+ const authenticated = await isAuthenticated(request);
+
+ if (!authenticated) {
+ // Return unauthenticated status
+ return json({ isAuthenticated: false });
+ }
+
+ // Get user data from session
+ const user = await getUserFromSession(request);
+
+ // Get current access token
+ const accessToken = await getAccessToken(request);
+
+ // Build response
+ return json({
+ isAuthenticated: true,
+ user,
+ // Don't expose the actual token to the client
+ tokenStatus: {
+ hasToken: Boolean(accessToken),
+ },
+ });
+ } catch (error) {
+ console.error('Error checking authentication status:', error);
+
+ // Return error response
+ return json(
+ {
+ isAuthenticated: false,
+ error: 'Failed to check authentication status'
+ },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/routes/auth.callback.tsx b/app/routes/auth.callback.tsx
new file mode 100644
index 00000000..0847780c
--- /dev/null
+++ b/app/routes/auth.callback.tsx
@@ -0,0 +1,135 @@
+import { json, redirect, type LoaderFunctionArgs, type MetaFunction } from '@remix-run/node';
+import { useLoaderData } from '@remix-run/react';
+import {
+ AuthError,
+ createUserSession,
+ exchangeCodeForToken,
+ fetchGitHubUser,
+ verifyState,
+} from '~/lib/auth/github-oauth.server';
+import BackgroundRays from '~/components/ui/BackgroundRays';
+
+export const meta: MetaFunction = () => {
+ return [
+ { title: 'Authenticating...' },
+ { name: 'description', content: 'Completing GitHub authentication' },
+ ];
+};
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ // Get URL parameters
+ const url = new URL(request.url);
+ const code = url.searchParams.get('code');
+ const state = url.searchParams.get('state');
+ const error = url.searchParams.get('error');
+ const errorDescription = url.searchParams.get('error_description');
+
+ // Get the redirect URL from state or default to home
+ const redirectTo = url.searchParams.get('redirectTo') || '/';
+
+ // Handle GitHub OAuth errors
+ if (error) {
+ return json({
+ success: false,
+ error: error,
+ errorDescription: errorDescription || 'An error occurred during authentication',
+ redirectTo: '/',
+ });
+ }
+
+ // Validate required parameters
+ if (!code || !state) {
+ return json({
+ success: false,
+ error: 'invalid_request',
+ errorDescription: 'Missing required parameters',
+ redirectTo: '/login',
+ });
+ }
+
+ try {
+ // Verify state parameter to prevent CSRF attacks
+ const cookieHeader = await verifyState(request, state);
+
+ // Exchange authorization code for access token
+ const { accessToken } = await exchangeCodeForToken(code, state);
+
+ // Fetch user data from GitHub API
+ const user = await fetchGitHubUser(accessToken);
+
+ // Create user session and redirect
+ return createUserSession(
+ request,
+ {
+ accessToken,
+ user,
+ },
+ redirectTo
+ );
+ } catch (error) {
+ console.error('Authentication error:', error);
+
+ let errorMessage = 'An unexpected error occurred during authentication';
+ let errorCode = 'server_error';
+
+ if (error instanceof AuthError) {
+ errorMessage = error.message;
+ errorCode = 'auth_error';
+ }
+
+ return json({
+ success: false,
+ error: errorCode,
+ errorDescription: errorMessage,
+ redirectTo: '/login',
+ });
+ }
+}
+
+export default function AuthCallback() {
+ const data = useLoaderData();
+
+ // Only render the component if we have error data
+ // On successful auth, the server will redirect immediately
+ if (data && 'success' in data && !data.success) {
+ return (
+
+
+
+
+
+
+
+
+
Authentication Failed
+
{data.errorDescription}
+
+
+
+ Redirecting you back in a moment...
+
+
+
+ );
+ }
+
+ // Loading state
+ return (
+
+
+
+
+
+
+
+
+
+
Authenticating...
+
+ Completing your sign-in with GitHub
+
+
+
+
+ );
+}
diff --git a/app/routes/auth.login.tsx b/app/routes/auth.login.tsx
new file mode 100644
index 00000000..1b94c478
--- /dev/null
+++ b/app/routes/auth.login.tsx
@@ -0,0 +1,162 @@
+import { json, redirect, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from '@remix-run/node';
+import { Form, useActionData, useLoaderData, useNavigation } from '@remix-run/react';
+import { useEffect, useState } from 'react';
+import { generateState, getAuthorizationUrl, isAuthenticated, storeState } from '~/lib/auth/github-oauth.server';
+import BackgroundRays from '~/components/ui/BackgroundRays';
+
+export const meta: MetaFunction = () => {
+ return [
+ { title: 'Login to Buildify' },
+ { name: 'description', content: 'Login to Buildify using your GitHub account' },
+ ];
+};
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ // Check if user is already authenticated
+ const authenticated = await isAuthenticated(request);
+ if (authenticated) {
+ // Redirect to home page if already logged in
+ return redirect('/');
+ }
+
+ // Get redirect URL from query params (if any)
+ const url = new URL(request.url);
+ const redirectTo = url.searchParams.get('redirectTo') || '/';
+
+ // Check if GitHub OAuth is configured
+ const clientId = process.env.GITHUB_CLIENT_ID;
+ const clientSecret = process.env.GITHUB_CLIENT_SECRET;
+ const isConfigured = Boolean(clientId && clientSecret);
+
+ return json({
+ redirectTo,
+ isConfigured,
+ error: isConfigured ? null : 'GitHub OAuth is not configured. Please set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET environment variables.',
+ });
+}
+
+export async function action({ request }: ActionFunctionArgs) {
+ // Check if GitHub OAuth is configured
+ const clientId = process.env.GITHUB_CLIENT_ID;
+ const clientSecret = process.env.GITHUB_CLIENT_SECRET;
+
+ if (!clientId || !clientSecret) {
+ return json(
+ { error: 'GitHub OAuth is not configured. Please set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET environment variables.' },
+ { status: 400 }
+ );
+ }
+
+ // Get form data
+ const formData = await request.formData();
+ const redirectTo = formData.get('redirectTo')?.toString() || '/';
+
+ try {
+ // Generate state for CSRF protection
+ const state = generateState();
+
+ // Get the callback URL
+ const url = new URL(request.url);
+ const callbackUrl = `${url.origin}/auth/callback`;
+
+ // Generate authorization URL
+ const { url: authorizationUrl } = getAuthorizationUrl(state, callbackUrl);
+
+ // Store state in session
+ const cookie = await storeState(request, state);
+
+ // Redirect to GitHub authorization URL
+ return redirect(authorizationUrl, {
+ headers: {
+ 'Set-Cookie': cookie,
+ },
+ });
+ } catch (error) {
+ console.error('Error initiating GitHub OAuth flow:', error);
+ return json(
+ { error: 'Failed to initiate login. Please try again later.' },
+ { status: 500 }
+ );
+ }
+}
+
+export default function Login() {
+ const { redirectTo, isConfigured, error } = useLoaderData();
+ const actionData = useActionData();
+ const navigation = useNavigation();
+ const [errorMessage, setErrorMessage] = useState(null);
+
+ // Set error message from loader or action data
+ useEffect(() => {
+ setErrorMessage(error || actionData?.error || null);
+ }, [error, actionData]);
+
+ const isSubmitting = navigation.state === 'submitting';
+
+ return (
+
+
+
+
+
+
Login to Buildify
+
+ Connect with your GitHub account to get started
+
+
+
+ {errorMessage && (
+
+ {errorMessage}
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/app/routes/auth.logout.tsx b/app/routes/auth.logout.tsx
new file mode 100644
index 00000000..21e5e066
--- /dev/null
+++ b/app/routes/auth.logout.tsx
@@ -0,0 +1,138 @@
+import { json, redirect, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from '@remix-run/node';
+import { Form, useLoaderData, useNavigation } from '@remix-run/react';
+import { useEffect } from 'react';
+import { logout, getUserFromSession } from '~/lib/auth/github-oauth.server';
+import { updateProfile } from '~/lib/stores/profile';
+import BackgroundRays from '~/components/ui/BackgroundRays';
+
+export const meta: MetaFunction = () => {
+ return [
+ { title: 'Logout from Buildify' },
+ { name: 'description', content: 'Logout from your Buildify account' },
+ ];
+};
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ // Get URL parameters
+ const url = new URL(request.url);
+ const redirectTo = url.searchParams.get('redirectTo') || '/';
+
+ // Get user data to display in the confirmation page
+ const user = await getUserFromSession(request);
+
+ return json({
+ redirectTo,
+ user,
+ });
+}
+
+export async function action({ request }: ActionFunctionArgs) {
+ // Get form data
+ const formData = await request.formData();
+ const redirectTo = formData.get('redirectTo')?.toString() || '/';
+
+ // Perform logout and redirect
+ return logout(request, redirectTo);
+}
+
+export default function Logout() {
+ const { redirectTo, user } = useLoaderData();
+ const navigation = useNavigation();
+ const isSubmitting = navigation.state === 'submitting';
+
+ // Reset profile store on client side when logging out via form submission
+ useEffect(() => {
+ if (isSubmitting) {
+ // Reset profile to empty values
+ updateProfile({
+ username: '',
+ bio: '',
+ avatar: '',
+ });
+ }
+ }, [isSubmitting]);
+
+ return (
+
+
+
+
+
+
Logout
+ {user ? (
+
+
+
+ {user.name || user.login}
+
+
+ {user.login}
+
+
+ ) : (
+
+ You are not currently logged in.
+
+ )}
+
+
+ {user ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/app/routes/profile.tsx b/app/routes/profile.tsx
new file mode 100644
index 00000000..920c7043
--- /dev/null
+++ b/app/routes/profile.tsx
@@ -0,0 +1,315 @@
+import { json, redirect, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from '@remix-run/node';
+import { Form, useActionData, useLoaderData, useNavigation } from '@remix-run/react';
+import { useEffect, useState } from 'react';
+import { getUserFromSession, isAuthenticated, requireAuthentication } from '~/lib/auth/github-oauth.server';
+import { useAuth } from '~/lib/hooks/useAuth';
+import { updateProfile } from '~/lib/stores/profile';
+import BackgroundRays from '~/components/ui/BackgroundRays';
+
+export const meta: MetaFunction = () => {
+ return [
+ { title: 'Your Profile - Buildify' },
+ { name: 'description', content: 'Manage your Buildify profile and preferences' },
+ ];
+};
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ // Check if user is authenticated on the server side
+ const authenticated = await isAuthenticated(request);
+
+ if (!authenticated) {
+ // Redirect to login if not authenticated
+ return redirect('/auth/login?redirectTo=/profile');
+ }
+
+ // Get user data from session
+ const user = await getUserFromSession(request);
+
+ return json({ user });
+}
+
+export async function action({ request }: ActionFunctionArgs) {
+ // Require authentication for this action
+ const user = await requireAuthentication(request, '/auth/login?redirectTo=/profile');
+
+ // Get form data
+ const formData = await request.formData();
+ const bio = formData.get('bio')?.toString() || '';
+ const displayName = formData.get('displayName')?.toString() || user.login;
+ const theme = formData.get('theme')?.toString() || 'system';
+
+ // Here you would typically update the user data in your database
+ // Since we're using client-side storage, we'll just return the updated values
+ // and let the client update the profile store
+
+ return json({
+ success: true,
+ message: 'Profile updated successfully',
+ updates: {
+ bio,
+ displayName,
+ theme,
+ },
+ });
+}
+
+export default function Profile() {
+ const { user: serverUser } = useLoaderData();
+ const actionData = useActionData();
+ const navigation = useNavigation();
+ const { isAuthenticated, user, githubUser } = useAuth();
+
+ const [bio, setBio] = useState(user?.bio || '');
+ const [displayName, setDisplayName] = useState(user?.username || serverUser?.login || '');
+
+ const isSubmitting = navigation.state === 'submitting';
+ const lastLogin = user?.lastLogin ? new Date(user.lastLogin).toLocaleString() : 'Unknown';
+
+ // Update form values when user data changes
+ useEffect(() => {
+ if (user) {
+ setBio(user.bio || '');
+ setDisplayName(user.username || '');
+ }
+ }, [user]);
+
+ // Apply updates from action data
+ useEffect(() => {
+ if (actionData?.success && actionData.updates) {
+ updateProfile({
+ bio: actionData.updates.bio,
+ username: actionData.updates.displayName,
+ });
+
+ // Show success notification
+ // This could be replaced with a toast notification
+ const timer = setTimeout(() => {
+ const notification = document.getElementById('notification');
+ if (notification) {
+ notification.style.opacity = '0';
+ }
+ }, 3000);
+
+ return () => clearTimeout(timer);
+ }
+ }, [actionData]);
+
+ if (!isAuthenticated || !user) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
Your Profile
+
+ Manage your profile information and preferences
+
+
+
+ {actionData?.success && (
+
+ {actionData.message}
+
+ )}
+
+
+ {/* GitHub Profile Information */}
+
+
+
+
+
+
+ {displayName}
+
+
+
+ @{githubUser?.login || serverUser?.login}
+
+
+ {githubUser?.html_url && (
+
+
+ GitHub Profile
+
+ )}
+
+
+
+
Email
+
+ {githubUser?.email || serverUser?.email || 'Not available'}
+
+
+
+
+
Authentication Status
+
+
+ Authenticated with GitHub
+
+
+
+
+
Last Login
+
{lastLogin}
+
+
+
+
+
+
+ {/* Profile Edit Form */}
+
+
+
+ {/* Account Management */}
+
+
+ Account Management
+
+
+
+
+
+
Sign Out
+
+ Sign out from your current session
+
+
+
+ Sign Out
+
+
+
+
+
+
GitHub Connection
+
+ Manage your GitHub connection settings
+
+
+
+ Manage
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/package-lock.json b/package-lock.json
index f2830724..f43dbca8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -37,6 +37,7 @@
"@iconify-json/svg-spinners": "^1.2.1",
"@lezer/highlight": "^1.2.1",
"@nanostores/react": "^0.7.3",
+ "@octokit/oauth-app": "^8.0.1",
"@octokit/rest": "^21.0.2",
"@octokit/types": "^13.6.2",
"@openrouter/ai-sdk-provider": "^0.0.5",
@@ -60,6 +61,7 @@
"@remix-run/react": "^2.15.2",
"@tanstack/react-virtual": "^3.13.0",
"@types/react-beautiful-dnd": "^13.1.8",
+ "@types/uuid": "^10.0.0",
"@uiw/codemirror-theme-vscode": "^4.23.6",
"@unocss/reset": "^0.61.9",
"@webcontainer/api": "1.6.1-internal.1",
@@ -115,6 +117,7 @@
"tailwind-merge": "^2.2.1",
"unist-util-visit": "^5.0.0",
"use-debounce": "^10.0.4",
+ "uuid": "^11.1.0",
"vite-plugin-node-polyfills": "^0.22.0",
"zod": "^3.24.1",
"zustand": "^5.0.3"
@@ -1044,6 +1047,25 @@
"node": ">=18.0.0"
}
},
+ "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@types/uuid": {
+ "version": "9.0.8",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
+ "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
+ "license": "MIT"
+ },
+ "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/@aws-sdk/client-sso": {
"version": "3.821.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.821.0.tgz",
@@ -4680,6 +4702,269 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
+ "node_modules/@octokit/auth-oauth-app": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-9.0.1.tgz",
+ "integrity": "sha512-TthWzYxuHKLAbmxdFZwFlmwVyvynpyPmjwc+2/cI3cvbT7mHtsAW9b1LvQaNnAuWL+pFnqtxdmrU8QpF633i1g==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-oauth-device": "^8.0.1",
+ "@octokit/auth-oauth-user": "^6.0.0",
+ "@octokit/request": "^10.0.2",
+ "@octokit/types": "^14.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/endpoint": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz",
+ "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^14.0.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": {
+ "version": "25.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
+ "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/request": {
+ "version": "10.0.2",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.2.tgz",
+ "integrity": "sha512-iYj4SJG/2bbhh+iIpFmG5u49DtJ4lipQ+aPakjL9OKpsGY93wM8w06gvFbEQxcMsZcCvk5th5KkIm2m8o14aWA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/endpoint": "^11.0.0",
+ "@octokit/request-error": "^7.0.0",
+ "@octokit/types": "^14.0.0",
+ "fast-content-type-parse": "^3.0.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/request-error": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz",
+ "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^14.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/types": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
+ "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^25.1.0"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-app/node_modules/fast-content-type-parse": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
+ "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/@octokit/auth-oauth-device": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-8.0.1.tgz",
+ "integrity": "sha512-TOqId/+am5yk9zor0RGibmlqn4V0h8vzjxlw/wYr3qzkQxl8aBPur384D1EyHtqvfz0syeXji4OUvKkHvxk/Gw==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/oauth-methods": "^6.0.0",
+ "@octokit/request": "^10.0.2",
+ "@octokit/types": "^14.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/endpoint": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz",
+ "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^14.0.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": {
+ "version": "25.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
+ "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/request": {
+ "version": "10.0.2",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.2.tgz",
+ "integrity": "sha512-iYj4SJG/2bbhh+iIpFmG5u49DtJ4lipQ+aPakjL9OKpsGY93wM8w06gvFbEQxcMsZcCvk5th5KkIm2m8o14aWA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/endpoint": "^11.0.0",
+ "@octokit/request-error": "^7.0.0",
+ "@octokit/types": "^14.0.0",
+ "fast-content-type-parse": "^3.0.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/request-error": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz",
+ "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^14.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/types": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
+ "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^25.1.0"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-device/node_modules/fast-content-type-parse": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
+ "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/@octokit/auth-oauth-user": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-6.0.0.tgz",
+ "integrity": "sha512-GV9IW134PHsLhtUad21WIeP9mlJ+QNpFd6V9vuPWmaiN25HEJeEQUcS4y5oRuqCm9iWDLtfIs+9K8uczBXKr6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-oauth-device": "^8.0.1",
+ "@octokit/oauth-methods": "^6.0.0",
+ "@octokit/request": "^10.0.2",
+ "@octokit/types": "^14.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/endpoint": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz",
+ "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^14.0.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/openapi-types": {
+ "version": "25.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
+ "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/request": {
+ "version": "10.0.2",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.2.tgz",
+ "integrity": "sha512-iYj4SJG/2bbhh+iIpFmG5u49DtJ4lipQ+aPakjL9OKpsGY93wM8w06gvFbEQxcMsZcCvk5th5KkIm2m8o14aWA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/endpoint": "^11.0.0",
+ "@octokit/request-error": "^7.0.0",
+ "@octokit/types": "^14.0.0",
+ "fast-content-type-parse": "^3.0.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/request-error": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz",
+ "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^14.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/types": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
+ "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^25.1.0"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-user/node_modules/fast-content-type-parse": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
+ "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/@octokit/auth-token": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz",
@@ -4689,6 +4974,46 @@
"node": ">= 18"
}
},
+ "node_modules/@octokit/auth-unauthenticated": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-7.0.1.tgz",
+ "integrity": "sha512-qVq1vdjLLZdE8kH2vDycNNjuJRCD1q2oet1nA/GXWaYlpDxlR7rdVhX/K/oszXslXiQIiqrQf+rdhDlA99JdTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/request-error": "^7.0.0",
+ "@octokit/types": "^14.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-unauthenticated/node_modules/@octokit/openapi-types": {
+ "version": "25.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
+ "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/auth-unauthenticated/node_modules/@octokit/request-error": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz",
+ "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^14.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-unauthenticated/node_modules/@octokit/types": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
+ "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^25.1.0"
+ }
+ },
"node_modules/@octokit/core": {
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.5.tgz",
@@ -4779,6 +5104,240 @@
"@octokit/openapi-types": "^25.1.0"
}
},
+ "node_modules/@octokit/oauth-app": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-8.0.1.tgz",
+ "integrity": "sha512-QnhMYEQpnYbEPn9cae+wXL2LuPMFglmfeuDJXXsyxIXdoORwkLK8y0cHhd/5du9MbO/zdG/BXixzB7EEwU63eQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-oauth-app": "^9.0.1",
+ "@octokit/auth-oauth-user": "^6.0.0",
+ "@octokit/auth-unauthenticated": "^7.0.1",
+ "@octokit/core": "^7.0.2",
+ "@octokit/oauth-authorization-url": "^8.0.0",
+ "@octokit/oauth-methods": "^6.0.0",
+ "@types/aws-lambda": "^8.10.83",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/oauth-app/node_modules/@octokit/auth-token": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz",
+ "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/oauth-app/node_modules/@octokit/core": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.2.tgz",
+ "integrity": "sha512-ODsoD39Lq6vR6aBgvjTnA3nZGliknKboc9Gtxr7E4WDNqY24MxANKcuDQSF0jzapvGb3KWOEDrKfve4HoWGK+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-token": "^6.0.0",
+ "@octokit/graphql": "^9.0.1",
+ "@octokit/request": "^10.0.2",
+ "@octokit/request-error": "^7.0.0",
+ "@octokit/types": "^14.0.0",
+ "before-after-hook": "^4.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/oauth-app/node_modules/@octokit/endpoint": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz",
+ "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^14.0.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/oauth-app/node_modules/@octokit/graphql": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.1.tgz",
+ "integrity": "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/request": "^10.0.2",
+ "@octokit/types": "^14.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/oauth-app/node_modules/@octokit/openapi-types": {
+ "version": "25.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
+ "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/oauth-app/node_modules/@octokit/request": {
+ "version": "10.0.2",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.2.tgz",
+ "integrity": "sha512-iYj4SJG/2bbhh+iIpFmG5u49DtJ4lipQ+aPakjL9OKpsGY93wM8w06gvFbEQxcMsZcCvk5th5KkIm2m8o14aWA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/endpoint": "^11.0.0",
+ "@octokit/request-error": "^7.0.0",
+ "@octokit/types": "^14.0.0",
+ "fast-content-type-parse": "^3.0.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/oauth-app/node_modules/@octokit/request-error": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz",
+ "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^14.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/oauth-app/node_modules/@octokit/types": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
+ "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^25.1.0"
+ }
+ },
+ "node_modules/@octokit/oauth-app/node_modules/before-after-hook": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
+ "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@octokit/oauth-app/node_modules/fast-content-type-parse": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
+ "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/@octokit/oauth-authorization-url": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-8.0.0.tgz",
+ "integrity": "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/oauth-methods": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-6.0.0.tgz",
+ "integrity": "sha512-Q8nFIagNLIZgM2odAraelMcDssapc+lF+y3OlcIPxyAU+knefO8KmozGqfnma1xegRDP4z5M73ABsamn72bOcA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/oauth-authorization-url": "^8.0.0",
+ "@octokit/request": "^10.0.2",
+ "@octokit/request-error": "^7.0.0",
+ "@octokit/types": "^14.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/oauth-methods/node_modules/@octokit/endpoint": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz",
+ "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^14.0.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": {
+ "version": "25.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
+ "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/oauth-methods/node_modules/@octokit/request": {
+ "version": "10.0.2",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.2.tgz",
+ "integrity": "sha512-iYj4SJG/2bbhh+iIpFmG5u49DtJ4lipQ+aPakjL9OKpsGY93wM8w06gvFbEQxcMsZcCvk5th5KkIm2m8o14aWA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/endpoint": "^11.0.0",
+ "@octokit/request-error": "^7.0.0",
+ "@octokit/types": "^14.0.0",
+ "fast-content-type-parse": "^3.0.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/oauth-methods/node_modules/@octokit/request-error": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz",
+ "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^14.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/oauth-methods/node_modules/@octokit/types": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
+ "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^25.1.0"
+ }
+ },
+ "node_modules/@octokit/oauth-methods/node_modules/fast-content-type-parse": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
+ "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/@octokit/openapi-types": {
"version": "24.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
@@ -7179,6 +7738,19 @@
"node": ">=18.0.0"
}
},
+ "node_modules/@smithy/middleware-retry/node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/@smithy/middleware-serde": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.8.tgz",
@@ -7735,6 +8307,12 @@
"@types/estree": "*"
}
},
+ "node_modules/@types/aws-lambda": {
+ "version": "8.10.149",
+ "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.149.tgz",
+ "integrity": "sha512-NXSZIhfJjnXqJgtS7IwutqIF/SOy1Wz5Px4gUY1RWITp3AYTyuJS4xaXr/bIJY1v15XMzrJ5soGnPM+7uigZjA==",
+ "license": "MIT"
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -8055,9 +8633,9 @@
"license": "MIT"
},
"node_modules/@types/uuid": {
- "version": "9.0.8",
- "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
- "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"license": "MIT"
},
"node_modules/@types/verror": {
@@ -31350,16 +31928,16 @@
}
},
"node_modules/uuid": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
- "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
+ "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
- "uuid": "dist/bin/uuid"
+ "uuid": "dist/esm/bin/uuid"
}
},
"node_modules/uvu": {
diff --git a/package.json b/package.json
index 9051a74a..14d218be 100644
--- a/package.json
+++ b/package.json
@@ -72,6 +72,7 @@
"@iconify-json/svg-spinners": "^1.2.1",
"@lezer/highlight": "^1.2.1",
"@nanostores/react": "^0.7.3",
+ "@octokit/oauth-app": "^8.0.1",
"@octokit/rest": "^21.0.2",
"@octokit/types": "^13.6.2",
"@openrouter/ai-sdk-provider": "^0.0.5",
@@ -95,6 +96,7 @@
"@remix-run/react": "^2.15.2",
"@tanstack/react-virtual": "^3.13.0",
"@types/react-beautiful-dnd": "^13.1.8",
+ "@types/uuid": "^10.0.0",
"@uiw/codemirror-theme-vscode": "^4.23.6",
"@unocss/reset": "^0.61.9",
"@webcontainer/api": "1.6.1-internal.1",
@@ -150,6 +152,7 @@
"tailwind-merge": "^2.2.1",
"unist-util-visit": "^5.0.0",
"use-debounce": "^10.0.4",
+ "uuid": "^11.1.0",
"vite-plugin-node-polyfills": "^0.22.0",
"zod": "^3.24.1",
"zustand": "^5.0.3"