Skip to content

Commit

Permalink
Added user accounts, not perfect, share links are broken by this commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jordan-dalby committed Nov 17, 2024
1 parent b924de5 commit 8191392
Show file tree
Hide file tree
Showing 24 changed files with 1,015 additions and 371 deletions.
11 changes: 7 additions & 4 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom';
import { AuthProvider } from './contexts/AuthContext';
import { useAuth } from './hooks/useAuth';
import { LoginPage } from './components/auth/LoginPage';
import { RegisterPage } from './components/auth/RegisterPage';
import { ROUTES } from './constants/routes';
import { PageContainer } from './components/common/layout/PageContainer';
import { ToastProvider } from './contexts/ToastContext';
Expand All @@ -11,7 +12,7 @@ import SharedSnippetView from './components/snippets/share/SharedSnippetView';
import SnippetPage from './components/snippets/view/SnippetPage';

const AuthenticatedApp: React.FC = () => {
const { isAuthenticated, isAuthRequired, isLoading } = useAuth();
const { isAuthenticated, isLoading } = useAuth();

if (isLoading) {
return (
Expand All @@ -23,8 +24,8 @@ const AuthenticatedApp: React.FC = () => {
);
}

if (isAuthRequired && !isAuthenticated) {
return <LoginPage />;
if (!isAuthenticated) {
return <Navigate to={ROUTES.LOGIN} replace />;
}

return <SnippetStorage />;
Expand All @@ -36,6 +37,8 @@ const App: React.FC = () => {
<ToastProvider>
<AuthProvider>
<Routes>
<Route path={ROUTES.LOGIN} element={<LoginPage />} />
<Route path={ROUTES.REGISTER} element={<RegisterPage />} />
<Route path={ROUTES.SHARED_SNIPPET} element={<SharedSnippetView />} />
<Route path={ROUTES.SNIPPET} element={<SnippetPage />} />
<Route path={ROUTES.HOME} element={<AuthenticatedApp />} />
Expand Down
23 changes: 19 additions & 4 deletions client/src/components/auth/LoginPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { Link, Navigate } from 'react-router-dom';
import { useAuth } from '../../hooks/useAuth';
import { PageContainer } from '../common/layout/PageContainer';
import { login as loginApi } from '../../utils/api/auth';
Expand All @@ -8,17 +9,23 @@ export const LoginPage: React.FC = () => {
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const { login } = useAuth();
const { login, isAuthenticated, authConfig } = useAuth();

if (isAuthenticated) {
return <Navigate to="/" replace />;
}

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setError('');

try {
const token = await loginApi(username, password);
login(token);
} catch (err) {
const { token, user } = await loginApi(username, password);
if (token && user) {
login(token, user);
}
} catch (err: any) {
setError('Invalid username or password');
} finally {
setIsLoading(false);
Expand All @@ -34,6 +41,14 @@ export const LoginPage: React.FC = () => {
</h2>
<p className="mt-2 text-center text-sm text-gray-400">
Please sign in to continue
{authConfig?.allowNewAccounts && (
<>
{' or '}
<Link to="/register" className="text-blue-400 hover:text-blue-300">
create an account
</Link>
</>
)}
</p>
</div>

Expand Down
152 changes: 152 additions & 0 deletions client/src/components/auth/RegisterPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { useAuth } from '../../hooks/useAuth';
import { register } from '../../utils/api/auth';
import { PageContainer } from '../common/layout/PageContainer';

export const RegisterPage: React.FC = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const { login, authConfig } = useAuth();

if (!authConfig?.allowNewAccounts) {
return (
<PageContainer className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h2 className="text-2xl font-bold mb-4">Registration Disabled</h2>
<p className="text-gray-400 mb-4">New account registration is currently disabled.</p>
<Link
to="/login"
className="text-blue-400 hover:text-blue-300 underline"
>
Return to Login
</Link>
</div>
</PageContainer>
);
}

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setIsLoading(true);

if (password !== confirmPassword) {
setError('Passwords do not match');
setIsLoading(false);
return;
}

try {
const response = await register(username, password);
if (response.token && response.user) {
login(response.token, response.user);
}
} catch (err: any) {
const errorMessage = err.message || 'Failed to register';
setError(errorMessage);
} finally {
setIsLoading(false);
}
};

return (
<PageContainer className="flex items-center justify-center min-h-screen">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-bold text-white">
Create Account
</h2>
<p className="mt-2 text-center text-sm text-gray-400">
Or{' '}
<Link to="/login" className="text-blue-400 hover:text-blue-300">
sign in to your account
</Link>
</p>
</div>

<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
{error && (
<div className="text-red-500 text-sm text-center bg-red-500/10 py-2 px-4 rounded-md border border-red-500/20">
{error}
</div>
)}

<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="username" className="sr-only">Username</label>
<input
id="username"
type="text"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border
border-gray-700 placeholder-gray-500 text-white bg-gray-800 rounded-t-md
focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
disabled={isLoading}
/>
</div>
<div>
<label htmlFor="password" className="sr-only">Password</label>
<input
id="password"
type="password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border
border-gray-700 placeholder-gray-500 text-white bg-gray-800
focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={isLoading}
/>
</div>
<div>
<label htmlFor="confirm-password" className="sr-only">Confirm Password</label>
<input
id="confirm-password"
type="password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border
border-gray-700 placeholder-gray-500 text-white bg-gray-800 rounded-b-md
focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Confirm Password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
disabled={isLoading}
/>
</div>
</div>

<div>
<button
type="submit"
className="group relative w-full flex justify-center py-2 px-4 border border-transparent
text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500
disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isLoading}
>
{isLoading ? (
<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>
Creating Account...
</span>
) : (
'Create Account'
)}
</button>
</div>
</form>
</div>
</PageContainer>
);
};
44 changes: 44 additions & 0 deletions client/src/components/auth/UserDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, { useRef, useState } from 'react';
import { LogOut, User } from 'lucide-react';
import { useAuth } from '../../hooks/useAuth';
import { useOutsideClick } from '../../hooks/useOutsideClick';

export const UserDropdown: React.FC = () => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const { user, logout } = useAuth();

useOutsideClick(dropdownRef, () => setIsOpen(false));

return (
<div ref={dropdownRef} className="relative">
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 px-3 py-1.5 bg-gray-800 hover:bg-gray-700
rounded-md transition-colors text-sm"
>
<User size={16} />
<span>{user?.username}</span>
</button>

{isOpen && (
<div
className="absolute right-0 mt-1 w-48 bg-gray-800 rounded-md shadow-lg
border border-gray-700 py-1 z-50"
>
<button
onClick={() => {
setIsOpen(false);
logout();
}}
className="w-full px-4 py-2 text-sm text-left text-gray-300 hover:bg-gray-700
flex items-center gap-2"
>
<LogOut size={16} />
<span>Sign out</span>
</button>
</div>
)}
</div>
);
};
19 changes: 3 additions & 16 deletions client/src/components/snippets/view/SnippetStorage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, { useState, useMemo, useCallback, useEffect } from 'react';
import { Loader2, LogOut } from 'lucide-react';
import { useAuth } from '../../../hooks/useAuth';
import { Loader2 } from 'lucide-react';
import { useSnippets } from '../../../hooks/useSnippets';
import { useSettings } from '../../../hooks/useSettings';
import { Snippet } from '../../../types/snippets';
Expand All @@ -12,12 +11,12 @@ import SettingsModal from '../../settings/SettingsModal';
import { ShareMenu } from '../share/ShareMenu';
import SnippetModal from './SnippetModal';
import { PageContainer } from '../../common/layout/PageContainer';
import { UserDropdown } from '../../auth/UserDropdown';

const APP_VERSION = "1.4.1";

const SnippetStorage: React.FC = () => {
const { snippets, isLoading, addSnippet, updateSnippet, removeSnippet, reloadSnippets } = useSnippets();
const { logout, isAuthRequired } = useAuth();
const {
viewMode, setViewMode, compactView, showCodePreview,
previewLines, includeCodeInSearch, updateSettings,
Expand Down Expand Up @@ -52,10 +51,6 @@ const SnippetStorage: React.FC = () => {
});
}, []);

const handleLogout = () => {
logout();
};

const openShareMenu = useCallback((snippet: Snippet) => {
setSnippetToShare(snippet);
setIsShareMenuOpen(true);
Expand Down Expand Up @@ -181,15 +176,7 @@ const SnippetStorage: React.FC = () => {
<h1 className="text-4xl font-bold text-gray-100">ByteStash</h1>
<span className="text-sm text-gray-400 mb-0">v{APP_VERSION}</span>
</div>
{isAuthRequired && (
<button
onClick={handleLogout}
className="flex items-center gap-2 px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-md transition-colors"
>
<LogOut size={18} />
<span>Logout</span>
</button>
)}
<UserDropdown />
</div>

<SearchAndFilter
Expand Down
3 changes: 2 additions & 1 deletion client/src/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export const ROUTES = {
HOME: '/',
SHARED_SNIPPET: '/s/:shareId',
SNIPPET: '/snippets/:snippetId',
LOGIN: '/login'
LOGIN: '/login',
REGISTER: '/register'
} as const;
Loading

0 comments on commit 8191392

Please sign in to comment.