feat: add password-gated video access for clients
- Videos page now requires a shared password to access - Login page repurposed as password entry (password: higgy2024) - Auth state persisted in localStorage (remembers forever) - Header shows Client Login / Log Out based on auth state - Removed signup page (not needed with shared password) - Password stored as SHA-256 hash (not plaintext) To change the password: printf 'newpassword' | sha256sum Then update VALID_HASH in lib/auth.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a7a626c66a
commit
02074d43e4
|
|
@ -1,50 +1,83 @@
|
|||
import type { Metadata } from "next"
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import Link from "next/link"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Log In",
|
||||
description: "Log in to your Higgy's Android Boxes account to access video tutorials and support.",
|
||||
}
|
||||
import { checkPassword, setAuthenticated } from "@/lib/auth"
|
||||
|
||||
export default function LoginPage() {
|
||||
const [password, setPassword] = useState("")
|
||||
const [error, setError] = useState("")
|
||||
const [loading, setLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setError("")
|
||||
setLoading(true)
|
||||
|
||||
const valid = await checkPassword(password)
|
||||
if (valid) {
|
||||
setAuthenticated(true)
|
||||
router.push("/videos")
|
||||
} else {
|
||||
setError("Incorrect password. Please try again.")
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center py-12 px-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-2xl font-bold">Welcome back</CardTitle>
|
||||
<CardTitle className="text-2xl font-bold">Client Access</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your credentials to access your video tutorials
|
||||
Enter the password provided by Higgy to access video tutorials
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input id="password" type="password" />
|
||||
</div>
|
||||
<Button className="w-full bg-[#8BC34A] hover:bg-[#7CB342] text-white">
|
||||
Log In
|
||||
</Button>
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
Don't have an account?{" "}
|
||||
<Link
|
||||
href="/signup"
|
||||
className="text-[#8BC34A] hover:underline font-medium"
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter your access password"
|
||||
required
|
||||
/>
|
||||
{error && (
|
||||
<p className="text-sm text-red-500">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-[#8BC34A] hover:bg-[#7CB342] text-white"
|
||||
disabled={loading}
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
{loading ? "Checking..." : "Access Videos"}
|
||||
</Button>
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
Don't have a password?{" "}
|
||||
<Link
|
||||
href="/"
|
||||
className="text-[#8BC34A] hover:underline font-medium"
|
||||
>
|
||||
Contact Higgy
|
||||
</Link>
|
||||
</p>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export default function HomePage() {
|
|||
className="bg-[#8BC34A] hover:bg-[#7CB342] text-white"
|
||||
asChild
|
||||
>
|
||||
<Link href="/signup">Get Started</Link>
|
||||
<Link href="/login">Get Started</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="lg" asChild>
|
||||
<Link href="/videos">Watch Tutorials</Link>
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
import type { Metadata } from "next"
|
||||
import Link from "next/link"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Sign Up",
|
||||
description: "Create your Higgy's Android Boxes account to access exclusive video tutorials and support.",
|
||||
}
|
||||
|
||||
export default function SignupPage() {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center py-12 px-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-2xl font-bold">Create an account</CardTitle>
|
||||
<CardDescription>
|
||||
Sign up to access exclusive video tutorials and support
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Full Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="Daniel Higginson"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input id="password" type="password" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirm-password">Confirm Password</Label>
|
||||
<Input id="confirm-password" type="password" />
|
||||
</div>
|
||||
<Button className="w-full bg-[#8BC34A] hover:bg-[#7CB342] text-white">
|
||||
Sign Up
|
||||
</Button>
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
Already have an account?{" "}
|
||||
<Link
|
||||
href="/login"
|
||||
className="text-[#8BC34A] hover:underline font-medium"
|
||||
>
|
||||
Log in
|
||||
</Link>
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -22,11 +22,5 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
|||
changeFrequency: "yearly",
|
||||
priority: 0.3,
|
||||
},
|
||||
{
|
||||
url: "https://higgysandroidboxes.com/signup",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "yearly",
|
||||
priority: 0.5,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import type { Metadata } from "next"
|
||||
import { VideoCard } from "@/components/video-card"
|
||||
import { videos } from "@/lib/data"
|
||||
import { VideosContent } from "./videos-content"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Video Tutorials",
|
||||
|
|
@ -9,24 +8,5 @@ export const metadata: Metadata = {
|
|||
}
|
||||
|
||||
export default function VideosPage() {
|
||||
return (
|
||||
<div className="min-h-screen py-12 px-4">
|
||||
<div className="container mx-auto max-w-6xl">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-3xl lg:text-4xl font-bold mb-4">
|
||||
Video Tutorials
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Learn how to get the most out of your Android box with our
|
||||
step-by-step video guides
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{videos.map((video) => (
|
||||
<VideoCard key={video.id} {...video} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return <VideosContent />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
"use client"
|
||||
|
||||
import { AuthGate } from "@/components/auth-gate"
|
||||
import { VideoCard } from "@/components/video-card"
|
||||
import { videos } from "@/lib/data"
|
||||
|
||||
export function VideosContent() {
|
||||
return (
|
||||
<AuthGate>
|
||||
<div className="min-h-screen py-12 px-4">
|
||||
<div className="container mx-auto max-w-6xl">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-3xl lg:text-4xl font-bold mb-4">
|
||||
Video Tutorials
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Learn how to get the most out of your Android box with our
|
||||
step-by-step video guides
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{videos.map((video) => (
|
||||
<VideoCard key={video.id} {...video} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthGate>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { isAuthenticated } from "@/lib/auth"
|
||||
|
||||
export function AuthGate({ children }: { children: React.ReactNode }) {
|
||||
const [checked, setChecked] = useState(false)
|
||||
const [authed, setAuthed] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated()) {
|
||||
setAuthed(true)
|
||||
} else {
|
||||
router.replace("/login")
|
||||
}
|
||||
setChecked(true)
|
||||
}, [router])
|
||||
|
||||
if (!checked) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="animate-pulse text-muted-foreground">Loading...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!authed) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
|
|
@ -1,7 +1,25 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { isAuthenticated, setAuthenticated } from "@/lib/auth"
|
||||
|
||||
export function Header() {
|
||||
const [authed, setAuthed] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
setAuthed(isAuthenticated())
|
||||
}, [])
|
||||
|
||||
function handleLogout() {
|
||||
setAuthenticated(false)
|
||||
setAuthed(false)
|
||||
router.push("/")
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container mx-auto flex h-16 items-center justify-between px-4">
|
||||
|
|
@ -18,12 +36,21 @@ export function Header() {
|
|||
<Button variant="ghost" asChild>
|
||||
<Link href="/videos">Videos</Link>
|
||||
</Button>
|
||||
<Button variant="ghost" asChild>
|
||||
<Link href="/login">Log In</Link>
|
||||
</Button>
|
||||
<Button asChild className="bg-[#8BC34A] hover:bg-[#7CB342] text-white">
|
||||
<Link href="/signup">Sign Up</Link>
|
||||
</Button>
|
||||
{authed ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={handleLogout}
|
||||
>
|
||||
Log Out
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
asChild
|
||||
className="bg-[#8BC34A] hover:bg-[#7CB342] text-white"
|
||||
>
|
||||
<Link href="/login">Client Login</Link>
|
||||
</Button>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
// Shared password hash (SHA-256 of the password)
|
||||
// To change the password:
|
||||
// 1. Run: printf 'newpassword' | sha256sum
|
||||
// 2. Replace VALID_HASH below with the output
|
||||
//
|
||||
// Current password: higgy2024
|
||||
const VALID_HASH =
|
||||
"4e58a7a86830a9ae2aae7fc8253c290a4773b1dbbc4e71ffa3eb4afbec3acb25"
|
||||
|
||||
const STORAGE_KEY = "higgys_auth"
|
||||
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
const encoder = new TextEncoder()
|
||||
const data = encoder.encode(password)
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", data)
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("")
|
||||
}
|
||||
|
||||
export async function checkPassword(password: string): Promise<boolean> {
|
||||
const hash = await hashPassword(password)
|
||||
return hash === VALID_HASH
|
||||
}
|
||||
|
||||
export function isAuthenticated(): boolean {
|
||||
if (typeof window === "undefined") return false
|
||||
return localStorage.getItem(STORAGE_KEY) === "true"
|
||||
}
|
||||
|
||||
export function setAuthenticated(value: boolean) {
|
||||
if (typeof window === "undefined") return
|
||||
if (value) {
|
||||
localStorage.setItem(STORAGE_KEY, "true")
|
||||
} else {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue