mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
feat: implement GitHub OIDC authentication
- Add GitHub OAuth 2.0 authentication flow - Create authentication routes: /auth/login, /auth/callback, /auth/logout - Implement OAuth server utilities with proper session management - Add authentication hooks and client-side state management - Update header with login/logout controls and user menu - Create user profile page with GitHub integration - Add environment variables for GitHub OAuth configuration - Include comprehensive documentation for setup and usage - Enhance profile store with authentication state - Add authentication status API endpoint Closes: GitHub authentication implementation
This commit is contained in:
19
.env.example
19
.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
|
||||
|
||||
@@ -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 (
|
||||
<header
|
||||
className={classNames('flex items-center p-5 border-b h-[var(--header-height)]', {
|
||||
@@ -23,20 +25,95 @@ export function Header() {
|
||||
<img src="/logo-dark-styled.png" alt="logo" className="w-[90px] inline-block hidden dark:block" />
|
||||
</a>
|
||||
</div>
|
||||
{chat.started && ( // Display ChatDescription and HeaderActionButtons only when the chat has started.
|
||||
<>
|
||||
<span className="flex-1 px-4 truncate text-center text-bolt-elements-textPrimary">
|
||||
<ClientOnly>{() => <ChatDescription />}</ClientOnly>
|
||||
</span>
|
||||
|
||||
{chat.started && (
|
||||
<span className="flex-1 px-4 truncate text-center text-bolt-elements-textPrimary">
|
||||
<ClientOnly>{() => <ChatDescription />}</ClientOnly>
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="flex items-center ml-auto">
|
||||
{/* Authentication Controls - Always visible */}
|
||||
<ClientOnly>
|
||||
{() => <AuthControls />}
|
||||
</ClientOnly>
|
||||
|
||||
{/* Existing Action Buttons - Only when chat has started */}
|
||||
{chat.started && (
|
||||
<ClientOnly>
|
||||
{() => (
|
||||
<div className="mr-1">
|
||||
<div className="ml-2">
|
||||
<HeaderActionButtons />
|
||||
</div>
|
||||
)}
|
||||
</ClientOnly>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthControls() {
|
||||
const { isAuthenticated, isLoading, user, login, logout } = useAuth();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center h-8 px-2 text-bolt-elements-textTertiary">
|
||||
<div className="i-svg-spinners:270-ring-with-bg w-5 h-5" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isAuthenticated && user) {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className="relative group">
|
||||
<button
|
||||
className="flex items-center gap-2 px-2 py-1 rounded-md hover:bg-bolt-elements-item-backgroundActive transition-colors"
|
||||
aria-label="User menu"
|
||||
>
|
||||
<img
|
||||
src={user.avatar}
|
||||
alt={`${user.username}'s avatar`}
|
||||
className="w-8 h-8 rounded-full border border-bolt-elements-borderColor"
|
||||
/>
|
||||
<span className="text-sm font-medium text-bolt-elements-textPrimary hidden sm:block">
|
||||
{user.username}
|
||||
</span>
|
||||
<div className="i-ph:caret-down w-4 h-4 text-bolt-elements-textTertiary" />
|
||||
</button>
|
||||
|
||||
<div className="absolute right-0 mt-1 w-48 py-1 bg-bolt-elements-background-depth-2 rounded-md shadow-lg border border-bolt-elements-borderColor hidden group-hover:block z-50">
|
||||
<div className="px-4 py-2 text-sm text-bolt-elements-textSecondary border-b border-bolt-elements-borderColor">
|
||||
Signed in as <span className="font-semibold text-bolt-elements-textPrimary">{user.username}</span>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
to="/profile"
|
||||
className="block px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive"
|
||||
>
|
||||
Your profile
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={() => 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
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => 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"
|
||||
>
|
||||
<div className="i-ph:sign-in w-4 h-4" />
|
||||
Sign in
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
241
app/lib/auth/github-oauth.server.ts
Normal file
241
app/lib/auth/github-oauth.server.ts
Normal file
@@ -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<GitHubUser> {
|
||||
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<GitHubUser | null> {
|
||||
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<string | null> {
|
||||
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<boolean> {
|
||||
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),
|
||||
},
|
||||
});
|
||||
}
|
||||
134
app/lib/hooks/useAuth.ts
Normal file
134
app/lib/hooks/useAuth.ts
Normal file
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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<Profile>(initialProfile);
|
||||
|
||||
/**
|
||||
* Update profile with partial data
|
||||
*/
|
||||
export const updateProfile = (updates: Partial<Profile>) => {
|
||||
profileStore.set({ ...profileStore.get(), ...updates });
|
||||
|
||||
@@ -26,3 +57,57 @@ export const updateProfile = (updates: Partial<Profile>) => {
|
||||
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;
|
||||
};
|
||||
|
||||
57
app/routes/api.auth.status.ts
Normal file
57
app/routes/api.auth.status.ts
Normal file
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
135
app/routes/auth.callback.tsx
Normal file
135
app/routes/auth.callback.tsx
Normal file
@@ -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<typeof loader>();
|
||||
|
||||
// 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 (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-bolt-elements-background-depth-1">
|
||||
<BackgroundRays />
|
||||
|
||||
<div className="w-full max-w-md p-8 space-y-8 bg-bolt-elements-background-depth-2 rounded-lg shadow-lg text-center">
|
||||
<div className="text-red-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-16 w-16 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<h1 className="text-2xl font-bold mt-4">Authentication Failed</h1>
|
||||
<p className="mt-2 text-bolt-content-secondary">{data.errorDescription}</p>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-bolt-content-tertiary">
|
||||
Redirecting you back in a moment...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Loading state
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-bolt-elements-background-depth-1">
|
||||
<BackgroundRays />
|
||||
|
||||
<div className="w-full max-w-md p-8 space-y-8 bg-bolt-elements-background-depth-2 rounded-lg shadow-lg text-center">
|
||||
<div className="text-bolt-content-primary">
|
||||
<svg className="animate-spin h-16 w-16 mx-auto text-bolt-content-accent" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<h1 className="text-2xl font-bold mt-4">Authenticating...</h1>
|
||||
<p className="mt-2 text-bolt-content-secondary">
|
||||
Completing your sign-in with GitHub
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
162
app/routes/auth.login.tsx
Normal file
162
app/routes/auth.login.tsx
Normal file
@@ -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<typeof loader>();
|
||||
const actionData = useActionData<typeof action>();
|
||||
const navigation = useNavigation();
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Set error message from loader or action data
|
||||
useEffect(() => {
|
||||
setErrorMessage(error || actionData?.error || null);
|
||||
}, [error, actionData]);
|
||||
|
||||
const isSubmitting = navigation.state === 'submitting';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-bolt-elements-background-depth-1">
|
||||
<BackgroundRays />
|
||||
|
||||
<div className="w-full max-w-md p-8 space-y-8 bg-bolt-elements-background-depth-2 rounded-lg shadow-lg">
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl font-bold text-bolt-content-primary">Login to Buildify</h1>
|
||||
<p className="mt-2 text-bolt-content-secondary">
|
||||
Connect with your GitHub account to get started
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{errorMessage && (
|
||||
<div className="p-4 text-sm text-red-500 bg-red-100 rounded-md">
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Form method="post" className="space-y-6">
|
||||
<input type="hidden" name="redirectTo" value={redirectTo} />
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isConfigured || isSubmitting}
|
||||
className={`
|
||||
w-full flex items-center justify-center py-3 px-4 rounded-md
|
||||
${isConfigured
|
||||
? 'bg-[#2da44e] hover:bg-[#2c974b] text-white'
|
||||
: 'bg-gray-300 text-gray-500 cursor-not-allowed'}
|
||||
transition-colors duration-200 font-medium
|
||||
`}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<span className="flex items-center">
|
||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Connecting...
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center">
|
||||
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385c.6.105.825-.255.825-.57c0-.285-.015-1.23-.015-2.235c-3.015.555-3.795-.735-4.035-1.41c-.135-.345-.72-1.41-1.23-1.695c-.42-.225-1.02-.78-.015-.795c.945-.015 1.62.87 1.845 1.23c1.08 1.815 2.805 1.305 3.495.99c.105-.78.42-1.305.765-1.605c-2.67-.3-5.46-1.335-5.46-5.925c0-1.305.465-2.385 1.23-3.225c-.12-.3-.54-1.53.12-3.18c0 0 1.005-.315 3.3 1.23c.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23c.66 1.65.24 2.88.12 3.18c.765.84 1.23 1.905 1.23 3.225c0 4.605-2.805 5.625-5.475 5.925c.435.375.81 1.095.81 2.22c0 1.605-.015 2.895-.015 3.3c0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
||||
</svg>
|
||||
Continue with GitHub
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</Form>
|
||||
|
||||
<div className="text-center text-sm text-bolt-content-tertiary">
|
||||
<p>
|
||||
By logging in, you agree to the{' '}
|
||||
<a href="#" className="text-bolt-content-accent hover:underline">
|
||||
Terms of Service
|
||||
</a>{' '}
|
||||
and{' '}
|
||||
<a href="#" className="text-bolt-content-accent hover:underline">
|
||||
Privacy Policy
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
138
app/routes/auth.logout.tsx
Normal file
138
app/routes/auth.logout.tsx
Normal file
@@ -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<typeof loader>();
|
||||
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 (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-bolt-elements-background-depth-1">
|
||||
<BackgroundRays />
|
||||
|
||||
<div className="w-full max-w-md p-8 space-y-8 bg-bolt-elements-background-depth-2 rounded-lg shadow-lg">
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl font-bold text-bolt-content-primary">Logout</h1>
|
||||
{user ? (
|
||||
<div className="mt-4 flex flex-col items-center">
|
||||
<img
|
||||
src={user.avatar_url}
|
||||
alt={`${user.login}'s avatar`}
|
||||
className="w-16 h-16 rounded-full border-2 border-bolt-elements-border-primary"
|
||||
/>
|
||||
<p className="mt-2 text-bolt-content-primary font-medium">
|
||||
{user.name || user.login}
|
||||
</p>
|
||||
<p className="text-sm text-bolt-content-secondary">
|
||||
{user.login}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-2 text-bolt-content-secondary">
|
||||
You are not currently logged in.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{user ? (
|
||||
<Form method="post" className="space-y-6">
|
||||
<input type="hidden" name="redirectTo" value={redirectTo} />
|
||||
|
||||
<p className="text-center text-bolt-content-secondary">
|
||||
Are you sure you want to log out?
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col space-y-3">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full py-3 px-4 bg-red-500 hover:bg-red-600 text-white rounded-md transition-colors duration-200 font-medium"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<span className="flex items-center justify-center">
|
||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Logging out...
|
||||
</span>
|
||||
) : (
|
||||
"Yes, log me out"
|
||||
)}
|
||||
</button>
|
||||
|
||||
<a
|
||||
href={redirectTo}
|
||||
className="w-full py-3 px-4 bg-transparent border border-bolt-elements-border-primary text-bolt-content-primary rounded-md transition-colors duration-200 font-medium text-center"
|
||||
>
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
</Form>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
<a
|
||||
href="/login"
|
||||
className="block w-full py-3 px-4 bg-[#2da44e] hover:bg-[#2c974b] text-white rounded-md transition-colors duration-200 font-medium text-center"
|
||||
>
|
||||
Log in
|
||||
</a>
|
||||
|
||||
<a
|
||||
href={redirectTo}
|
||||
className="block w-full py-3 px-4 bg-transparent border border-bolt-elements-border-primary text-bolt-content-primary rounded-md transition-colors duration-200 font-medium text-center"
|
||||
>
|
||||
Return to home
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
315
app/routes/profile.tsx
Normal file
315
app/routes/profile.tsx
Normal file
@@ -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<typeof loader>();
|
||||
const actionData = useActionData<typeof action>();
|
||||
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 (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-bolt-elements-background-depth-1">
|
||||
<BackgroundRays />
|
||||
<div className="w-full max-w-md p-8 bg-bolt-elements-background-depth-2 rounded-lg shadow-lg">
|
||||
<div className="text-center">
|
||||
<div className="i-svg-spinners:270-ring-with-bg w-12 h-12 mx-auto text-bolt-content-accent" />
|
||||
<h1 className="text-2xl font-bold mt-4 text-bolt-content-primary">Loading profile...</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-bolt-elements-background-depth-1">
|
||||
<BackgroundRays />
|
||||
|
||||
<div className="container mx-auto px-4 py-8 max-w-4xl">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-bolt-content-primary">Your Profile</h1>
|
||||
<p className="text-bolt-content-secondary">
|
||||
Manage your profile information and preferences
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{actionData?.success && (
|
||||
<div
|
||||
id="notification"
|
||||
className="mb-6 p-4 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-100 rounded-md transition-opacity duration-500"
|
||||
>
|
||||
{actionData.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{/* GitHub Profile Information */}
|
||||
<div className="col-span-1">
|
||||
<div className="bg-bolt-elements-background-depth-2 rounded-lg shadow-md p-6 border border-bolt-elements-borderColor">
|
||||
<div className="flex flex-col items-center">
|
||||
<img
|
||||
src={user.avatar || githubUser?.avatar_url}
|
||||
alt={`${displayName}'s avatar`}
|
||||
className="w-24 h-24 rounded-full border-2 border-bolt-elements-borderColor"
|
||||
/>
|
||||
|
||||
<h2 className="mt-4 text-xl font-semibold text-bolt-content-primary">
|
||||
{displayName}
|
||||
</h2>
|
||||
|
||||
<p className="text-sm text-bolt-content-secondary">
|
||||
@{githubUser?.login || serverUser?.login}
|
||||
</p>
|
||||
|
||||
{githubUser?.html_url && (
|
||||
<a
|
||||
href={githubUser.html_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-2 text-sm text-bolt-content-accent hover:underline flex items-center"
|
||||
>
|
||||
<span className="i-ph:github-logo mr-1" />
|
||||
GitHub Profile
|
||||
</a>
|
||||
)}
|
||||
|
||||
<div className="mt-6 w-full">
|
||||
<div className="py-2 border-t border-bolt-elements-borderColor">
|
||||
<h3 className="text-sm font-medium text-bolt-content-secondary">Email</h3>
|
||||
<p className="text-bolt-content-primary">
|
||||
{githubUser?.email || serverUser?.email || 'Not available'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="py-2 border-t border-bolt-elements-borderColor">
|
||||
<h3 className="text-sm font-medium text-bolt-content-secondary">Authentication Status</h3>
|
||||
<div className="flex items-center mt-1">
|
||||
<span className="inline-block w-2 h-2 rounded-full bg-green-500 mr-2"></span>
|
||||
<span className="text-bolt-content-primary">Authenticated with GitHub</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="py-2 border-t border-bolt-elements-borderColor">
|
||||
<h3 className="text-sm font-medium text-bolt-content-secondary">Last Login</h3>
|
||||
<p className="text-bolt-content-primary">{lastLogin}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Profile Edit Form */}
|
||||
<div className="col-span-1 md:col-span-2">
|
||||
<div className="bg-bolt-elements-background-depth-2 rounded-lg shadow-md p-6 border border-bolt-elements-borderColor">
|
||||
<h2 className="text-xl font-semibold text-bolt-content-primary mb-4">
|
||||
Edit Profile
|
||||
</h2>
|
||||
|
||||
<Form method="post" className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="displayName" className="block text-sm font-medium text-bolt-content-secondary mb-1">
|
||||
Display Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="displayName"
|
||||
name="displayName"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded-md text-bolt-content-primary focus:outline-none focus:ring-2 focus:ring-bolt-content-accent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="bio" className="block text-sm font-medium text-bolt-content-secondary mb-1">
|
||||
Bio
|
||||
</label>
|
||||
<textarea
|
||||
id="bio"
|
||||
name="bio"
|
||||
rows={4}
|
||||
value={bio}
|
||||
onChange={(e) => setBio(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded-md text-bolt-content-primary focus:outline-none focus:ring-2 focus:ring-bolt-content-accent"
|
||||
placeholder="Tell us about yourself..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="theme" className="block text-sm font-medium text-bolt-content-secondary mb-1">
|
||||
Theme Preference
|
||||
</label>
|
||||
<select
|
||||
id="theme"
|
||||
name="theme"
|
||||
className="w-full px-3 py-2 bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded-md text-bolt-content-primary focus:outline-none focus:ring-2 focus:ring-bolt-content-accent"
|
||||
defaultValue="system"
|
||||
>
|
||||
<option value="system">System Default</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
</select>
|
||||
<p className="mt-1 text-xs text-bolt-content-tertiary">
|
||||
Theme preferences will be applied across all your sessions
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className={`
|
||||
w-full sm:w-auto px-4 py-2 rounded-md font-medium
|
||||
${isSubmitting
|
||||
? 'bg-bolt-elements-background-depth-3 text-bolt-content-tertiary cursor-not-allowed'
|
||||
: 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent hover:bg-bolt-elements-item-backgroundAccentHover'}
|
||||
transition-colors duration-200
|
||||
`}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<span className="flex items-center justify-center">
|
||||
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Saving...
|
||||
</span>
|
||||
) : (
|
||||
'Save Changes'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
{/* Account Management */}
|
||||
<div className="mt-6 bg-bolt-elements-background-depth-2 rounded-lg shadow-md p-6 border border-bolt-elements-borderColor">
|
||||
<h2 className="text-xl font-semibold text-bolt-content-primary mb-4">
|
||||
Account Management
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center py-2 border-b border-bolt-elements-borderColor">
|
||||
<div>
|
||||
<h3 className="font-medium text-bolt-content-primary">Sign Out</h3>
|
||||
<p className="text-sm text-bolt-content-secondary">
|
||||
Sign out from your current session
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="/auth/logout"
|
||||
className="px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-md transition-colors duration-200 text-sm font-medium"
|
||||
>
|
||||
Sign Out
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center py-2">
|
||||
<div>
|
||||
<h3 className="font-medium text-bolt-content-primary">GitHub Connection</h3>
|
||||
<p className="text-sm text-bolt-content-secondary">
|
||||
Manage your GitHub connection settings
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="/settings"
|
||||
className="px-4 py-2 bg-transparent border border-bolt-elements-borderColor hover:bg-bolt-elements-background-depth-3 text-bolt-content-primary rounded-md transition-colors duration-200 text-sm font-medium"
|
||||
>
|
||||
Manage
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
592
package-lock.json
generated
592
package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user