From d69bee427b2635a38aa8b56758d603140ae4dea3 Mon Sep 17 00:00:00 2001 From: mudabbir-ahmad Date: Tue, 11 Mar 2025 17:21:46 +0000 Subject: [PATCH] fixed a bit --- app/(auth)/login/page.js | 98 +++++--- app/(auth)/register/page.js | 93 +++++--- app/(main)/admin/page.js | 99 ++++++-- app/(main)/team-selection/page.js | 85 +++++-- app/actions/auth.js | 140 ++++++++++++ app/api/auth/[...nextauth]/route.js | 55 +++++ app/api/auth/[nextuath].js | 38 ---- app/api/user/route.js | 17 ++ app/globals.css | 338 ++++++++++++++++++++++++++++ clue_hunt.db | Bin 36864 -> 36864 bytes init-schema.sql | 60 +++++ lib/db.js | 79 +++++-- scripts/test-sqlite-fallback.js | 12 + 13 files changed, 961 insertions(+), 153 deletions(-) create mode 100644 app/actions/auth.js create mode 100644 app/api/auth/[...nextauth]/route.js delete mode 100644 app/api/auth/[nextuath].js create mode 100644 app/api/user/route.js create mode 100644 init-schema.sql create mode 100644 scripts/test-sqlite-fallback.js diff --git a/app/(auth)/login/page.js b/app/(auth)/login/page.js index 9379ed6..eca8ab9 100644 --- a/app/(auth)/login/page.js +++ b/app/(auth)/login/page.js @@ -2,52 +2,86 @@ import { useState } from 'react'; import { signIn } from 'next-auth/react'; import { useRouter } from 'next/navigation'; +import Link from 'next/link'; export default function Login() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); const router = useRouter(); const handleLogin = async (e) => { e.preventDefault(); - const result = await signIn('credentials', { - redirect: false, - username, - password, - }); + setIsLoading(true); + setError(''); - if (result.error) { - setError(result.error); - } else { - router.push('/team-selection'); // Redirect to the team selection page after login + try { + const result = await signIn('credentials', { + redirect: false, + username, + password, + }); + + if (result.error) { + setError('Invalid username or password'); + } else { + // Fetch the user role to redirect appropriately + const response = await fetch('/api/user'); + const userData = await response.json(); + + if (userData.role === 'admin') { + router.push('/admin'); + } else { + router.push('/team-selection'); + } + } + } catch (err) { + setError('An unexpected error occurred'); + console.error(err); + } finally { + setIsLoading(false); } }; return ( -
-

Login

-
- setUsername(e.target.value)} - required - /> - setPassword(e.target.value)} - required - /> - -
- {error &&

{error}

} -

- Don't have an account? Register -

+
+
+

Login

+
+
+ setUsername(e.target.value)} + required + className="auth-input" + /> +
+
+ setPassword(e.target.value)} + required + className="auth-input" + /> +
+ +
+ {error &&

{error}

} +

+ Don't have an account? Register +

+
); } \ No newline at end of file diff --git a/app/(auth)/register/page.js b/app/(auth)/register/page.js index b61225d..6bb5021 100644 --- a/app/(auth)/register/page.js +++ b/app/(auth)/register/page.js @@ -1,54 +1,79 @@ 'use client'; // Mark as a Client Component import { useState } from 'react'; import { useRouter } from 'next/navigation'; -import { query } from '../../../lib/db'; -import bcrypt from 'bcryptjs'; +import { registerUser } from '../../actions/auth'; +import Link from 'next/link'; export default function Register() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); const router = useRouter(); - const handleRegister = async (e) => { + async function handleSubmit(e) { e.preventDefault(); - const hashedPassword = bcrypt.hashSync(password, 10); + setIsLoading(true); + setError(''); try { - await query( - 'INSERT INTO users (username, password, role) VALUES (?, ?, ?)', - [username, hashedPassword, 'user'] - ); - router.push('/login'); // Redirect to the login page after registration + const formData = new FormData(); + formData.append('username', username); + formData.append('password', password); + + const result = await registerUser(formData); + + if (result.success) { + router.push('/login'); + } else { + setError(result.error || 'Registration failed'); + } } catch (err) { - setError('Registration failed. Username may already exist.'); + setError('An unexpected error occurred'); + console.error(err); + } finally { + setIsLoading(false); } - }; + } return ( -
-

Register

-
- setUsername(e.target.value)} - required - /> - setPassword(e.target.value)} - required - /> - -
- {error &&

{error}

} -

- Already have an account? Login -

+
+
+

Register

+
+
+ setUsername(e.target.value)} + required + className="auth-input" + /> +
+
+ setPassword(e.target.value)} + required + className="auth-input" + /> +
+ +
+ {error &&

{error}

} +

+ Already have an account? Login +

+
); } \ No newline at end of file diff --git a/app/(main)/admin/page.js b/app/(main)/admin/page.js index 30cd403..26e5fbc 100644 --- a/app/(main)/admin/page.js +++ b/app/(main)/admin/page.js @@ -1,26 +1,99 @@ 'use client'; // Mark as a Client Component -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useSession } from 'next-auth/react'; +import { useRouter } from 'next/navigation'; +import { startHuntTimer, getHuntStatus } from '../actions/auth'; export default function Admin() { - const { data: session } = useSession(); - const [timerStarted, setTimerStarted] = useState(false); + const { data: session, status } = useSession(); + const [timerActive, setTimerActive] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const router = useRouter(); - if (session?.user?.role !== 'admin') { - return

Access denied

; + // Load timer status on page load + useEffect(() => { + async function loadTimerStatus() { + try { + const status = await getHuntStatus(); + setTimerActive(status.active); + } catch (err) { + console.error('Failed to load timer status:', err); + setError('Failed to load timer status'); + } finally { + setLoading(false); + } + } + + if (status === 'authenticated') { + loadTimerStatus(); + } + }, [status]); + + // Check if user is admin + useEffect(() => { + if (status === 'authenticated' && session?.user?.role !== 'admin') { + router.push('/'); + } + }, [session, status, router]); + + // Handle access denied and loading states + if (status === 'loading' || loading) { + return
Loading...
; } - const startTimer = async () => { - // Implement timer logic (e.g., update database or state) - setTimerStarted(true); + if (status === 'unauthenticated') { + router.push('/login'); + return null; + } + + if (session?.user?.role !== 'admin') { + return null; // Already redirecting via useEffect + } + + const toggleTimer = async () => { + setLoading(true); + try { + const newStatus = !timerActive; + const result = await startHuntTimer(newStatus); + + if (result.success) { + setTimerActive(newStatus); + } else { + setError('Failed to update timer status'); + } + } catch (err) { + console.error('Failed to toggle timer:', err); + setError('Failed to toggle timer'); + } finally { + setLoading(false); + } }; return ( -
-

Admin Console

- +
+
+

Admin Controls

+ +
+

Hunt Timer

+

Status: + {timerActive ? 'Active' : 'Inactive'} +

+ +
+ +
+
+ + {error &&

{error}

} +
); } \ No newline at end of file diff --git a/app/(main)/team-selection/page.js b/app/(main)/team-selection/page.js index 39c6867..dce8e03 100644 --- a/app/(main)/team-selection/page.js +++ b/app/(main)/team-selection/page.js @@ -1,40 +1,81 @@ 'use client'; // Mark as a Client Component import { useState } from 'react'; +import { useSession } from 'next-auth/react'; import { useRouter } from 'next/navigation'; -import { query } from '../../../lib/db'; +import { joinTeam } from '../actions/auth'; export default function TeamSelection() { + const { data: session, status } = useSession(); const [teamCode, setTeamCode] = useState(''); const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); const router = useRouter(); + // Redirect if not authenticated + if (status === 'unauthenticated') { + router.push('/login'); + return null; + } + + // Redirect if user is admin + if (status === 'authenticated' && session?.user?.role === 'admin') { + router.push('/admin'); + return null; + } + const handleJoinTeam = async (e) => { e.preventDefault(); - const team = await query('SELECT * FROM teams WHERE code = ?', [teamCode]); - if (team) { - router.push(`/stats/${team.id}`); - } else { - setError('Team not found'); + setIsLoading(true); + setError(''); + + try { + const result = await joinTeam(session.user.id, teamCode); + + if (result.success) { + router.push(`/team/${result.teamId}`); + } else { + setError(result.error || 'Team not found'); + } + } catch (err) { + setError('An unexpected error occurred'); + console.error(err); + } finally { + setIsLoading(false); } }; + if (status === 'loading') { + return
Loading...
; + } + return ( -
-

Team Selection

-
- setTeamCode(e.target.value)} - required - /> - -
- {error &&

{error}

} -

- Create a new team -

+
+
+

Team Selection

+
+
+ setTeamCode(e.target.value)} + required + className="team-input" + /> +
+ +
+ {error &&

{error}

} + +
); } \ No newline at end of file diff --git a/app/actions/auth.js b/app/actions/auth.js new file mode 100644 index 0000000..53b61e1 --- /dev/null +++ b/app/actions/auth.js @@ -0,0 +1,140 @@ +// app/actions/auth.js +'use server' + +import { queryOne, run } from '../../lib/db'; +import bcrypt from 'bcryptjs'; + +// Register a new user +export async function registerUser(formData) { + try { + const username = formData.get('username'); + const password = formData.get('password'); + + // Check if user already exists + const existingUser = await queryOne( + 'SELECT * FROM users WHERE username = ?', + [username] + ); + + if (existingUser) { + return { success: false, error: 'Username already exists' }; + } + + // Hash the password + const hashedPassword = await bcrypt.hash(password, 10); + + // Insert the new user + const result = await run( + 'INSERT INTO users (username, password, role) VALUES (?, ?, ?)', + [username, hashedPassword, 'user'] + ); + + return { + success: true, + userId: result.lastID + }; + } catch (error) { + console.error('Registration error:', error); + return { + success: false, + error: 'Registration failed: ' + error.message + }; + } +} + +// Login function (to be used with NextAuth) +export async function validateCredentials(username, password) { + try { + const user = await queryOne( + 'SELECT * FROM users WHERE username = ?', + [username] + ); + + if (!user) { + return null; + } + + const isValid = await bcrypt.compare(password, user.password); + + if (!isValid) { + return null; + } + + return { + id: user.id, + username: user.username, + role: user.role + }; + } catch (error) { + console.error('Login error:', error); + return null; + } +} + +// Start hunt timer (admin only) +export async function startHuntTimer(active) { + try { + // First check if a record exists + const currentTimer = await queryOne('SELECT * FROM game_settings WHERE id = 1'); + + if (currentTimer) { + // Update existing record + await run( + 'UPDATE game_settings SET hunt_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1', + [active ? 1 : 0] + ); + } else { + // Create new record + await run( + 'INSERT INTO game_settings (id, hunt_active, created_at, updated_at) VALUES (1, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)', + [active ? 1 : 0] + ); + } + + return { success: true }; + } catch (error) { + console.error('Timer update error:', error); + return { success: false, error: error.message }; + } +} + +// Get hunt timer status +export async function getHuntStatus() { + try { + const settings = await queryOne('SELECT * FROM game_settings WHERE id = 1'); + return { + active: settings?.hunt_active === 1, + updatedAt: settings?.updated_at + }; + } catch (error) { + console.error('Get hunt status error:', error); + return { active: false }; + } +} + +// Join a team +export async function joinTeam(userId, teamCode) { + try { + // Find the team by code + const team = await queryOne('SELECT * FROM teams WHERE code = ?', [teamCode]); + + if (!team) { + return { success: false, error: 'Team not found' }; + } + + // Update user's team + await run( + 'UPDATE users SET team_id = ? WHERE id = ?', + [team.id, userId] + ); + + return { + success: true, + teamId: team.id, + teamName: team.name + }; + } catch (error) { + console.error('Join team error:', error); + return { success: false, error: error.message }; + } +} \ No newline at end of file diff --git a/app/api/auth/[...nextauth]/route.js b/app/api/auth/[...nextauth]/route.js new file mode 100644 index 0000000..d2cd90c --- /dev/null +++ b/app/api/auth/[...nextauth]/route.js @@ -0,0 +1,55 @@ +// app/api/auth/[...nextauth]/route.js +import NextAuth from 'next-auth'; +import CredentialsProvider from 'next-auth/providers/credentials'; +import { validateCredentials } from '../../../actions/auth'; + +export const authOptions = { + providers: [ + CredentialsProvider({ + name: 'Credentials', + credentials: { + username: { label: "Username", type: "text" }, + password: { label: "Password", type: "password" } + }, + async authorize(credentials) { + if (!credentials?.username || !credentials?.password) { + return null; + } + + const user = await validateCredentials( + credentials.username, + credentials.password + ); + + return user; + } + }) + ], + callbacks: { + async jwt({ token, user }) { + if (user) { + token.id = user.id; + token.role = user.role; + token.username = user.username; + } + return token; + }, + async session({ session, token }) { + if (token) { + session.user.id = token.id; + session.user.role = token.role; + session.user.username = token.username; + } + return session; + } + }, + pages: { + signIn: '/login', + }, + session: { + strategy: "jwt", + } +}; + +const handler = NextAuth(authOptions); +export { handler as GET, handler as POST }; \ No newline at end of file diff --git a/app/api/auth/[nextuath].js b/app/api/auth/[nextuath].js deleted file mode 100644 index b7cff8b..0000000 --- a/app/api/auth/[nextuath].js +++ /dev/null @@ -1,38 +0,0 @@ -import NextAuth from 'next-auth'; -import Providers from 'next-auth/providers'; -import bcrypt from 'bcryptjs'; -import { query } from '../../../lib/db'; - -export default NextAuth({ - providers: [ - Providers.Credentials({ - name: 'Credentials', - credentials: { - username: { label: 'Username', type: 'text' }, - password: { label: 'Password', type: 'password' }, - }, - authorize: async (credentials) => { - const user = await query('SELECT * FROM users WHERE username = ?', [credentials.username]); - if (user && bcrypt.compareSync(credentials.password, user.password)) { - return { id: user.id, name: user.username, role: user.role }; - } - return null; - }, - }), - ], - callbacks: { - jwt: async (token, user) => { - if (user) { - token.role = user.role; - } - return token; - }, - session: async (session, token) => { - session.user.role = token.role; - return session; - }, - }, - pages: { - signIn: '/login', // Redirect to the login page - }, -}); \ No newline at end of file diff --git a/app/api/user/route.js b/app/api/user/route.js new file mode 100644 index 0000000..0d34b8b --- /dev/null +++ b/app/api/user/route.js @@ -0,0 +1,17 @@ +// app/api/user/route.js +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '../auth/[...nextauth]/route'; + +export async function GET() { + const session = await getServerSession(authOptions); + + if (!session) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + return Response.json({ + id: session.user.id, + username: session.user.username, + role: session.user.role + }); +} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index fe94fe6..0a235ee 100644 --- a/app/globals.css +++ b/app/globals.css @@ -45,4 +45,342 @@ a:hover { .error { color: red; +} + + +/* Reset & Base Styles */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: Arial, sans-serif; + background-color: #f4f4f4; + color: #333; + min-height: 100vh; +} + +/* Typography */ +h1 { + color: #333; + margin-bottom: 1rem; + font-size: 1.5rem; + text-align: center; +} + +h2 { + color: #444; + margin-bottom: 0.75rem; + font-size: 1.25rem; +} + +p { + margin-bottom: 1rem; +} + +/* Layout Components */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 1rem; +} + +/* Card Styles */ +.auth-container, +.team-selection-container, +.admin-page, +.stats-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + padding: 1rem; +} + +.auth-card, +.team-selection-card, +.admin-card, +.stats-card { + background-color: #fff; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + padding: 2rem; + width: 100%; + max-width: 400px; +} + +/* Form Elements */ +.form-group { + margin-bottom: 1rem; +} + +input[type="text"], +input[type="password"], +.auth-input, +.team-input { + display: block; + width: 100%; + padding: 0.75rem; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 1rem; + margin-bottom: 1rem; +} + +button, +.auth-button, +.team-button { + display: block; + width: 100%; + padding: 0.75rem; + background-color: #0070f3; + color: white; + border: none; + border-radius: 4px; + font-size: 1rem; + cursor: pointer; + transition: background-color 0.2s; +} + +button:hover, +.auth-button:hover, +.team-button:hover { + background-color: #005bb5; +} + +button:disabled { + background-color: #ccc; + cursor: not-allowed; +} + +/* Admin Panel Styles */ +.timer-section { + margin-top: 1.5rem; + padding: 1rem; + border: 1px solid #eee; + border-radius: 4px; +} + +.active { + color: #22c55e; + font-weight: bold; +} + +.inactive { + color: #ef4444; + font-weight: bold; +} + +.start-button { + background-color: #22c55e; +} + +.start-button:hover { + background-color: #16a34a; +} + +.stop-button { + background-color: #ef4444; +} + +.stop-button:hover { + background-color: #dc2626; +} + +.control-buttons { + margin-top: 1rem; +} + +/* QR Scanner */ +.qr-scanner { + margin: 1rem 0; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +/* Links */ +a { + color: #0070f3; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +.auth-link { + margin-top: 1rem; + text-align: center; +} + +.create-team-link { + display: block; + margin-top: 1rem; + text-align: center; +} + +/* Error messages */ +.error { + color: #ef4444; + margin-top: 0.5rem; + font-size: 0.875rem; +} + +/* Loading state */ +.loading { + text-align: center; + font-size: 1rem; + padding: 2rem; +} + +/* Team page styled as in Figma */ +.team-page { + background-color: #f8e9a1; + padding: 1rem; + border-radius: 8px; +} + +.user-box { + background-color: #d99e6a; + border-radius: 8px; + padding: 0.75rem; + margin-bottom: 0.75rem; + color: #fff; +} + +.add-users { + display: flex; + align-items: center; + justify-content: space-between; + background-color: #f3cf55; + border-radius: 8px; + padding: 0.5rem 1rem; + margin-bottom: 1rem; +} + +/* Stats page styled as in Figma */ +.stats-user-box { + background-color: #d99e6a; + border-radius: 4px; + padding: 0.5rem; + margin-bottom: 0.5rem; +} + +.clue-box { + margin: 0.75rem 0; +} + +.clue-item { + display: flex; + justify-content: space-between; + padding: 0.5rem; + border-bottom: 1px solid #eee; +} + +.found { + color: #22c55e; +} + +.not-found { + color: #ef4444; +} + +/* Progress page styled as in Figma */ +.progress-page { + background-color: #8fbc8f; /* Green background */ + padding: 1rem; + border-radius: 8px; +} + +.progress-map { + position: relative; + margin-top: 1rem; + min-height: 200px; +} + +.progress-point { + width: 20px; + height: 20px; + border-radius: 50%; + background-color: #fff; + position: absolute; +} + +.progress-point.active { + background-color: #ff6b6b; +} + +/* Navigation bar at bottom of screens as in Figma */ +.nav-bar { + display: flex; + justify-content: space-around; + background-color: #f8f8f8; + border-top: 1px solid #ddd; + padding: 0.75rem 0; + position: fixed; + bottom: 0; + width: 100%; +} + +.nav-item { + display: flex; + flex-direction: column; + align-items: center; + color: #777; +} + +.nav-item.active { + color: #0070f3; +} + +.nav-icon { + font-size: 1.5rem; + margin-bottom: 0.25rem; +} + +/* Leaderboard */ +.leaderboard { + padding: 1rem; + background-color: #f8e9a1; + border-radius: 8px; +} + +.team-row { + background-color: #f3cf55; + padding: 0.75rem; + margin-bottom: 0.75rem; + border-radius: 4px; + display: flex; + justify-content: space-between; +} + +/* Clue page as in Figma */ +.clue-page { + background-color: #f3cf55; + padding: 1rem; + border-radius: 8px; +} + +.clue-box { + background-color: #fff8dc; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.clue-description { + margin-bottom: 1rem; +} + +.clue-buttons { + display: flex; + justify-content: space-between; + margin-top: 1rem; +} + +.clue-button { + width: 48%; } \ No newline at end of file diff --git a/clue_hunt.db b/clue_hunt.db index 66124d94d6b92e51e1f89d0dfb773fb90f26d948..7ba33dd73897c133c6a1b5ee1de0ff19cc9569bc 100644 GIT binary patch delta 148 zcmZozz|^pSX@WE(%S0JxMwX2UOZYjM_@^@PKj5FbSx}&p-`kU!jX}7PRWvC-NyR8h z#n3<{J;}nt)hx)O%E#3wB-Pz0tf0a)wAA0%!^FueEIXhw!^grYJjXNK)6FN#$uQqt zFCsj$va~q0Xz~Mj6`<+w8Th~RzuzpV@R*;2omrR>XfpHW@A|L!8JRafvX=`008?-* A { + if (err) { + console.error('Error opening database:', err.message); + } else { + console.log('Connected to the SQLite database.'); + } + }); +} + +// Helper function to run queries with results +async function query(sql, params = []) { + if (typeof window !== 'undefined') { + throw new Error('Database queries cannot be executed on the client side'); + } + + const db = getDb(); + return new Promise((resolve, reject) => { + db.all(sql, params, (err, rows) => { + if (err) { + reject(err); + } else { + resolve(rows); + } + db.close(); // Close the database connection + }); + }); +} + +// Helper for single row queries +async function queryOne(sql, params = []) { + const rows = await query(sql, params); + return rows && rows.length > 0 ? rows[0] : null; +} + +// Helper for insert/update/delete operations +async function run(sql, params = []) { + if (typeof window !== 'undefined') { + throw new Error('Database operations cannot be executed on the client side'); + } + + const db = getDb(); + return new Promise((resolve, reject) => { + db.run(sql, params, function(err) { + if (err) { + reject(err); + } else { + resolve({ lastID: this.lastID, changes: this.changes }); + } + db.close(); // Close the database connection + }); + }); +} + +module.exports = { query, queryOne, run }; \ No newline at end of file diff --git a/scripts/test-sqlite-fallback.js b/scripts/test-sqlite-fallback.js new file mode 100644 index 0000000..2e70b0f --- /dev/null +++ b/scripts/test-sqlite-fallback.js @@ -0,0 +1,12 @@ +const { query } = require('../lib/db'); + +async function testDb() { + try { + const rows = await query('SELECT 1 + 1 AS result'); + console.log('Database test successful:', rows); + } catch (err) { + console.error('Database test failed:', err); + } +} + +testDb(); \ No newline at end of file