diff options
| author | Pinapelz <yukais@pinapelz.com> | 2025-11-04 10:44:22 -0800 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2025-11-04 10:44:22 -0800 |
| commit | 945bf269d7b0c8f8b5245475398694378b74b25f (patch) | |
| tree | 2a6a235f021145b839078513d47fe64cae6511cd | |
| parent | 732f3873354863a4dec591d4d6a425edb7b47c61 (diff) | |
add bio for profile page
| -rw-r--r-- | backend/schema.prisma | 1 | ||||
| -rw-r--r-- | backend/src/routes/user.ts | 9 | ||||
| -rw-r--r-- | docs/SESSION_GUIDE.md | 221 | ||||
| -rw-r--r-- | frontend/src/pages/Profile.tsx | 36 | ||||
| -rw-r--r-- | frontend/src/utils/authApi.ts | 1 |
5 files changed, 40 insertions, 228 deletions
diff --git a/backend/schema.prisma b/backend/schema.prisma index 3dda29d..fbc2f90 100644 --- a/backend/schema.prisma +++ b/backend/schema.prisma @@ -13,6 +13,7 @@ model User { password String salt String email String @unique + bio String? sessions Session[] scores Score[] isAdmin Boolean diff --git a/backend/src/routes/user.ts b/backend/src/routes/user.ts index fc03e68..497d6da 100644 --- a/backend/src/routes/user.ts +++ b/backend/src/routes/user.ts @@ -4,12 +4,13 @@ import { prisma } from '../config/db'; export const handleMeRoute = async (req: express.Request, res: express.Response) => { try { - if (!req.session.userId) { - return res.status(403).json({ error: 'Not Authenticated' }); + const { userId } = req.query; + if (!userId) { + return res.status(400).json({ error: 'userId query parameter is required' }); } const user = await prisma.user.findUniqueOrThrow({ - where: { id: req.session.userId }, - select: { id: true, username: true, isAdmin: true } + where: { id: parseInt(userId as string) }, + select: { id: true, username: true, isAdmin: true, bio: true } }); const isAdmin = user.id === 1 || user.isAdmin; res.json({user, isAdmin}); diff --git a/docs/SESSION_GUIDE.md b/docs/SESSION_GUIDE.md deleted file mode 100644 index 1f9a228..0000000 --- a/docs/SESSION_GUIDE.md +++ /dev/null @@ -1,221 +0,0 @@ -# Session Management Guide - -This guide explains how to use the session-based authentication system implemented in the backend. - -## Overview - -The backend now supports persistent login sessions using: -- **express-session** for server-side session management -- **HTTP-only cookies** for secure session storage -- **Database session tracking** with automatic cleanup -- **CSRF protection** through HTTP-only cookies - -## Environment Setup - -1. Copy `.env.example` to `.env`: -```bash -cp .env.example .env -``` - -2. Update the `SESSION_SECRET` in your `.env` file with a secure random string: -``` -SESSION_SECRET="your-very-secure-secret-key-change-this-in-production-make-it-long-and-random" -``` - -## API Endpoints - -### Authentication Endpoints - -#### Register User -- **POST** `/api/register` -- **Body**: `{ username, password, name, email }` -- **Response**: User data + session created -- **Sets**: HTTP-only session cookie - -#### Login -- **POST** `/api/authenticate` -- **Body**: `{ username, password }` -- **Response**: User data + session created -- **Sets**: HTTP-only session cookie - -#### Logout -- **POST** `/api/logout` -- **Requires**: Valid session -- **Response**: Success message -- **Clears**: Session cookie and database sessions - -### Session Management - -#### Check Session Status -- **GET** `/api/session` -- **Response**: `{ authenticated: boolean, user?: UserData }` -- **Use**: Check if user is still logged in - -#### Get Current User -- **GET** `/api/me` -- **Requires**: Valid session -- **Response**: Current user data - -## Frontend Integration - -### Making Authenticated Requests - -Always include credentials in your fetch requests: - -```javascript -// Example fetch with credentials -const response = await fetch('http://localhost:5000/api/me', { - method: 'GET', - credentials: 'include', // This is crucial! - headers: { - 'Content-Type': 'application/json', - } -}); -``` - -### Login Flow Example - -```javascript -// Login -const loginResponse = await fetch('http://localhost:5000/api/authenticate', { - method: 'POST', - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - username: 'user@example.com', - password: 'password123' - }) -}); - -if (loginResponse.ok) { - const userData = await loginResponse.json(); - console.log('Logged in:', userData); - // Session cookie is automatically set -} -``` - -### Check Authentication Status - -```javascript -// Check if user is still logged in -const sessionResponse = await fetch('http://localhost:5000/api/session', { - method: 'GET', - credentials: 'include' -}); - -const sessionData = await sessionResponse.json(); -if (sessionData.authenticated) { - console.log('User is logged in:', sessionData.user); -} else { - console.log('User is not logged in'); -} -``` - -### Logout - -```javascript -// Logout -const logoutResponse = await fetch('http://localhost:5000/api/logout', { - method: 'POST', - credentials: 'include' -}); - -if (logoutResponse.ok) { - console.log('Logged out successfully'); - // Session cookie is automatically cleared -} -``` - -## Security Features - -### Session Security -- **HTTP-only cookies**: Prevents XSS attacks -- **Secure cookies**: Enabled in production (HTTPS) -- **SameSite protection**: Prevents CSRF attacks -- **Session expiration**: 24-hour automatic expiry - -### Database Sessions -- Sessions are stored in the database for server-side validation -- Expired sessions are automatically cleaned up every hour -- User logout removes all sessions for that user - -### Password Security -- Passwords are hashed with bcrypt (12 rounds) -- Unique salt per user for additional security -- Original password never stored - -## Middleware - -### `requireAuth` Middleware -Protects routes that need authentication: - -```javascript -app.get('/api/protected-route', requireAuth, (req, res) => { - // req.user contains the authenticated user data - const user = req.user; - res.json({ message: 'This is protected', user }); -}); -``` - -## Production Considerations - -### Environment Variables -```env -SESSION_SECRET="your-production-secret-minimum-32-characters-long" -NODE_ENV="production" -FRONTEND_URL="https://yourdomain.com" -``` - -### Security Settings -In production, ensure: -1. `SESSION_SECRET` is a strong, unique string -2. `secure: true` in cookie settings (HTTPS only) -3. Proper CORS origin configuration -4. HTTPS enabled for your domain - -### Database -- Consider using PostgreSQL or MySQL instead of SQLite -- Implement database connection pooling -- Set up database backups - -## Troubleshooting - -### Common Issues - -1. **Session not persisting**: Make sure `credentials: 'include'` is set in frontend requests -2. **CORS errors**: Verify CORS origin matches your frontend URL -3. **Session expires immediately**: Check system time and session expiration settings -4. **Authentication fails after server restart**: Sessions are stored in database, so they should persist - -### Debug Session Issues - -Check session status: -```javascript -fetch('http://localhost:5000/api/session', { - credentials: 'include' -}).then(r => r.json()).then(console.log); -``` - -### Clear All Sessions -If you need to force logout all users: -```sql -DELETE FROM Session; -``` - -## Database Schema - -The session system adds a `Session` table: - -```prisma -model Session { - id String @id @default(cuid()) - userId Int - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - createdAt DateTime @default(now()) - expiresAt DateTime -} -``` - -Sessions are automatically linked to users and cleaned up when users are deleted.
\ No newline at end of file diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index 4ab5995..cc8fc99 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx @@ -7,14 +7,44 @@ import { NavBar } from "../components/NavBar"; import { useAuth } from "../contexts/AuthContext"; import Heatmap from "../components/Heatmap"; import type { HeatmapData } from "../components/Heatmap"; +import type { User } from "../utils/authApi"; + +interface UserData { + user: User; + isAdmin: boolean; +} const Profile = () => { const { user, isLoading, logout } = useAuth(); const targetUser = new URLSearchParams(window.location.search).get("userId") || ""; // looking at profile of this user const navigate = useNavigate(); - const [fetchingHeatmapData, setFetchingHeatmapData] = useState(false); + const [fetchingHeatmapData, setFetchingHeatmapData] = useState(true); + const [fetchingUserData, setFetchingUserData] = useState(true); const [heatmapData, setHeatmapData] = useState<HeatmapData>({ data: [] }); + const [userData, setUserData] = useState<UserData | null>(null); + + useEffect(() => { + if (targetUser) { + setFetchingUserData(true); + const fetchUserData = async () => { + try { + const response = await fetch( + new URL( + import.meta.env.VITE_API_URL + "/me?userId=" + targetUser, + ), + { credentials: "include" }, + ); + const data = await response.json(); + setUserData(data); + setFetchingUserData(false); + } catch (error) { + console.error("Failed to fetch user data:", error); + } + }; + fetchUserData(); + } + }, [targetUser]); useEffect(() => { if (targetUser) { @@ -61,7 +91,7 @@ const Profile = () => { navigate("/"); } - if (isLoading || fetchingHeatmapData) { + if (isLoading || fetchingHeatmapData || fetchingUserData) { return <LoadingDisplay message="Loading Profile Page..." />; } @@ -95,7 +125,7 @@ const Profile = () => { {user.username} </h1> <p className="text-sm sm:text-base text-slate-400"> - This is a profile page for {user.username} + {userData?.user.bio || "I'm a fairly non-descript person"} </p> </div> {isBrowser ? ( diff --git a/frontend/src/utils/authApi.ts b/frontend/src/utils/authApi.ts index 1d6cc70..1dac129 100644 --- a/frontend/src/utils/authApi.ts +++ b/frontend/src/utils/authApi.ts @@ -156,6 +156,7 @@ export interface User { username: string; email: string; isAdmin: boolean; + bio?: string; } export interface SessionResponse { |
