feat: add guest mode, bug fixes, and self-hosted config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
M1ngdaXie
2026-03-15 09:45:17 +00:00
parent 763575f284
commit 9c19769eb0
15 changed files with 187 additions and 36 deletions

View File

@@ -1,6 +1,15 @@
import type { User } from '../types/auth';
import { API_BASE_URL, authFetch } from './client';
export async function guestLogin(): Promise<string> {
const res = await fetch(`${API_BASE_URL}/auth/guest`, { method: 'POST' });
if (!res.ok) {
throw new Error('Failed to create guest session');
}
const data = await res.json();
return data.token;
}
export const authApi = {
getCurrentUser: async (): Promise<User> => {
const response = await authFetch(`${API_BASE_URL}/auth/me`);

View File

@@ -24,7 +24,7 @@ function ProtectedRoute({ children }: ProtectedRouteProps) {
}
if (!user) {
return <Navigate to={`/login?redirect=${location.pathname}`} replace />;
return <Navigate to={`/login?redirect=${encodeURIComponent(location.pathname + location.search)}`} replace />;
}
return <>{children}</>;

View File

@@ -31,7 +31,7 @@ function ShareModal({
const [permission, setPermission] = useState<'view' | 'edit'>('view');
// Form state for link sharing
const [linkPermission, setLinkPermission] = useState<'view' | 'edit'>('view');
const [linkPermission, setLinkPermission] = useState<'view' | 'edit'>('edit');
const [copied, setCopied] = useState(false);
// Load shares on mount

View File

@@ -1,4 +1,4 @@
import { createContext, useContext, useState, useEffect } from 'react';
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
import type { ReactNode } from 'react';
import type { User, AuthContextType } from '../types/auth';
import { authApi } from '../api/auth';
@@ -40,7 +40,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
initAuth();
}, []);
const login = async (newToken: string) => {
const login = useCallback(async (newToken: string) => {
try {
setLoading(true);
setError(null);
@@ -60,7 +60,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
} finally {
setLoading(false);
}
};
}, []);
const logout = () => {
localStorage.removeItem('auth_token');

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
@@ -7,8 +7,12 @@ function AuthCallback() {
const navigate = useNavigate();
const { login } = useAuth();
const [error, setError] = useState<string | null>(null);
const processedRef = useRef(false);
useEffect(() => {
if (processedRef.current) return;
processedRef.current = true;
const handleCallback = async () => {
const token = searchParams.get('token');
const redirect =

View File

@@ -141,6 +141,19 @@
background: hsl(var(--surface));
}
.landing-login-button.guest {
background: transparent;
border: 1px dashed hsl(var(--border));
color: hsl(var(--text-secondary));
font-size: 0.9rem;
padding: 0.6rem 1.5rem;
}
.landing-login-button.guest:hover {
color: hsl(var(--text-primary));
border-style: solid;
}
.landing-login-button.large {
padding: 1rem 2rem;
font-size: 1.05rem;

View File

@@ -1,3 +1,7 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { guestLogin } from '../api/auth';
import FloatingGem from '../components/PixelSprites/FloatingGem';
import PixelIcon from '../components/PixelIcon/PixelIcon';
import DocNestLogo from '../assets/docnest/docnest-icon-128.png';
@@ -6,6 +10,10 @@ import { API_BASE_URL } from '../config';
import './LandingPage.css';
function LandingPage() {
const { login } = useAuth();
const navigate = useNavigate();
const [guestLoading, setGuestLoading] = useState(false);
const handleGoogleLogin = () => {
window.location.href = `${API_BASE_URL}/auth/google`;
};
@@ -14,6 +22,19 @@ function LandingPage() {
window.location.href = `${API_BASE_URL}/auth/github`;
};
const handleGuestLogin = async () => {
try {
setGuestLoading(true);
const token = await guestLogin();
await login(token);
navigate('/');
} catch (err) {
console.error('Guest login failed:', err);
} finally {
setGuestLoading(false);
}
};
return (
<div className="landing-page">
<div className="landing-theme-toggle">
@@ -61,6 +82,13 @@ function LandingPage() {
<span>Continue with GitHub</span>
</button>
</div>
<button
className="landing-login-button guest"
onClick={handleGuestLogin}
disabled={guestLoading}
>
{guestLoading ? 'Entering...' : 'Try as Guest'}
</button>
<p className="hero-note">No credit card required.</p>
</div>
</div>

View File

@@ -105,3 +105,36 @@
transform: translateY(0);
box-shadow: var(--shadow-sm);
}
.login-divider {
display: flex;
align-items: center;
gap: 12px;
margin: 4px 0;
}
.login-divider::before,
.login-divider::after {
content: '';
flex: 1;
height: 1px;
background: hsl(var(--border));
}
.login-divider span {
font-size: 13px;
color: hsl(var(--text-secondary));
white-space: nowrap;
}
.guest-button {
background: transparent;
border: 1px dashed hsl(var(--border));
color: hsl(var(--text-secondary));
}
.guest-button:hover {
background: hsl(var(--surface-hover, var(--border) / 0.1));
color: hsl(var(--text-primary));
border-style: solid;
}

View File

@@ -1,15 +1,17 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { guestLogin } from '../api/auth';
import { API_BASE_URL } from '../config';
import DocNestLogo from '../assets/docnest/docnest-icon-128.png';
import ThemeToggle from '../components/ThemeToggle';
import './LoginPage.css';
function LoginPage() {
const { user, loading } = useAuth();
const { user, loading, login } = useAuth();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [guestLoading, setGuestLoading] = useState(false);
useEffect(() => {
if (!loading && user) {
@@ -20,7 +22,7 @@ function LoginPage() {
const saveRedirectAndGo = (oauthUrl: string) => {
const redirect = searchParams.get('redirect');
if (redirect) {
sessionStorage.setItem('oauth_redirect', redirect);
sessionStorage.setItem('oauth_redirect', decodeURIComponent(redirect));
}
window.location.href = oauthUrl;
};
@@ -33,6 +35,20 @@ function LoginPage() {
saveRedirectAndGo(`${API_BASE_URL}/auth/github`);
};
const handleGuestLogin = async () => {
try {
setGuestLoading(true);
const token = await guestLogin();
await login(token);
const redirect = searchParams.get('redirect');
navigate(redirect ? decodeURIComponent(redirect) : '/');
} catch (err) {
console.error('Guest login failed:', err);
} finally {
setGuestLoading(false);
}
};
if (loading) {
return (
<div className="login-page">
@@ -93,6 +109,18 @@ function LoginPage() {
</svg>
Continue with GitHub
</button>
<div className="login-divider">
<span></span>
</div>
<button
className="login-button guest-button"
onClick={handleGuestLogin}
disabled={guestLoading}
>
{guestLoading ? 'Entering...' : 'Continue as Guest'}
</button>
</div>
</div>
</div>