Enable demo mode features, auto-fill demo credentials, and enhance branding integration

- Added `DEMO_MODE` to backend configuration with relaxed security support for specific demo accounts.
- Updated password validators to allow predefined weak passwords in demo mode.
- Auto-fill login forms with demo credentials via query parameters for improved demo accessibility.
- Introduced demo user creation logic during database initialization if `DEMO_MODE` is enabled.
- Replaced `img` tags with `next/image` for consistent and optimized visuals in branding elements.
- Refined footer, header, and layout components to incorporate improved logo handling.
This commit is contained in:
Felipe Cardoso
2025-11-21 07:42:40 +01:00
parent 0e34cab921
commit a410586cfb
14 changed files with 138 additions and 24 deletions

View File

@@ -17,6 +17,7 @@ BACKEND_PORT=8000
# Must be at least 32 characters # Must be at least 32 characters
SECRET_KEY=your_secret_key_here_REPLACE_WITH_GENERATED_KEY_32_CHARS_MIN SECRET_KEY=your_secret_key_here_REPLACE_WITH_GENERATED_KEY_32_CHARS_MIN
ENVIRONMENT=development ENVIRONMENT=development
DEMO_MODE=false
DEBUG=true DEBUG=true
BACKEND_CORS_ORIGINS=["http://localhost:3000"] BACKEND_CORS_ORIGINS=["http://localhost:3000"]
FIRST_SUPERUSER_EMAIL=admin@example.com FIRST_SUPERUSER_EMAIL=admin@example.com

View File

@@ -14,6 +14,10 @@ class Settings(BaseSettings):
default="development", default="development",
description="Environment: development, staging, or production", description="Environment: development, staging, or production",
) )
DEMO_MODE: bool = Field(
default=False,
description="Enable demo mode (relaxed security, demo users)",
)
# Security: Content Security Policy # Security: Content Security Policy
# Set to False to disable CSP entirely (not recommended) # Set to False to disable CSP entirely (not recommended)
@@ -110,11 +114,21 @@ class Settings(BaseSettings):
@field_validator("FIRST_SUPERUSER_PASSWORD") @field_validator("FIRST_SUPERUSER_PASSWORD")
@classmethod @classmethod
def validate_superuser_password(cls, v: str | None) -> str | None: def validate_superuser_password(cls, v: str | None, info) -> str | None:
"""Validate superuser password strength.""" """Validate superuser password strength."""
if v is None: if v is None:
return v return v
# Get environment from values if available
values_data = info.data if info.data else {}
demo_mode = values_data.get("DEMO_MODE", False)
if demo_mode:
# In demo mode, allow specific weak passwords for demo accounts
demo_passwords = {"Demo123!", "Admin123!"}
if v in demo_passwords:
return v
if len(v) < 12: if len(v) < 12:
raise ValueError("FIRST_SUPERUSER_PASSWORD must be at least 12 characters") raise ValueError("FIRST_SUPERUSER_PASSWORD must be at least 12 characters")

View File

@@ -57,6 +57,27 @@ async def init_db() -> User | None:
await session.refresh(user) await session.refresh(user)
logger.info(f"Created first superuser: {user.email}") logger.info(f"Created first superuser: {user.email}")
# Create demo user if in demo mode
if settings.DEMO_MODE:
demo_email = "demo@example.com"
demo_password = "Demo123!"
existing_demo_user = await user_crud.get_by_email(session, email=demo_email)
if not existing_demo_user:
demo_user_in = UserCreate(
email=demo_email,
password=demo_password,
first_name="Demo",
last_name="User",
is_superuser=False,
)
demo_user = await user_crud.create(session, obj_in=demo_user_in)
await session.commit()
logger.info(f"Created demo user: {demo_user.email}")
else:
logger.info(f"Demo user already exists: {existing_demo_user.email}")
return user return user
except Exception as e: except Exception as e:

View File

@@ -60,6 +60,15 @@ def validate_password_strength(password: str) -> str:
>>> validate_password_strength("MySecureP@ss123") # Valid >>> validate_password_strength("MySecureP@ss123") # Valid
>>> validate_password_strength("password1") # Invalid - too weak >>> validate_password_strength("password1") # Invalid - too weak
""" """
# Check if we are in demo mode
from app.core.config import settings
if settings.DEMO_MODE:
# In demo mode, allow specific weak passwords for demo accounts
demo_passwords = {"Demo123!", "Admin123!"}
if password in demo_passwords:
return password
# Check minimum length # Check minimum length
if len(password) < 12: if len(password) < 12:
raise ValueError("Password must be at least 12 characters long") raise ValueError("Password must be at least 12 characters long")

View File

@@ -12,6 +12,7 @@ The **PragmaStack** logo represents the core values of the project: structure, s
</div> </div>
### Icon ### Icon
For smaller contexts (favicons, headers), we use the simplified icon: For smaller contexts (favicons, headers), we use the simplified icon:
<div align="center"> <div align="center">

View File

@@ -6,8 +6,9 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { Link } from '@/lib/i18n/routing'; import { Link } from '@/lib/i18n/routing';
import { useSearchParams } from 'next/navigation';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod'; import { z } from 'zod';
@@ -82,6 +83,9 @@ export function LoginForm({
const [serverError, setServerError] = useState<string | null>(null); const [serverError, setServerError] = useState<string | null>(null);
const loginMutation = useLogin(); const loginMutation = useLogin();
// Get query parameters for demo auto-fill
const searchParams = useSearchParams();
const loginSchema = createLoginSchema((key: string) => { const loginSchema = createLoginSchema((key: string) => {
if (key.startsWith('validation.')) { if (key.startsWith('validation.')) {
return tValidation(key.replace('validation.', '')); return tValidation(key.replace('validation.', ''));
@@ -102,6 +106,15 @@ export function LoginForm({
}, },
}); });
// Auto-fill form from query params (for demo mode)
useEffect(() => {
const email = searchParams.get('email');
const password = searchParams.get('password');
if (email) form.setValue('email', email);
if (password) form.setValue('password', password);
}, [searchParams, form]);
const onSubmit = async (data: LoginFormData) => { const onSubmit = async (data: LoginFormData) => {
try { try {
// Clear previous errors // Clear previous errors

View File

@@ -8,10 +8,10 @@
'use client'; 'use client';
import Image from 'next/image';
import { Link } from '@/lib/i18n/routing'; import { Link } from '@/lib/i18n/routing';
import { usePathname } from '@/lib/i18n/routing'; import { usePathname } from '@/lib/i18n/routing';
import { import {
Palette, Palette,
LayoutDashboard, LayoutDashboard,
Box, Box,
@@ -94,7 +94,13 @@ export function DevLayout({ children }: DevLayoutProps) {
<div className="flex h-14 items-center justify-between gap-6"> <div className="flex h-14 items-center justify-between gap-6">
{/* Left: Logo + Badge */} {/* Left: Logo + Badge */}
<div className="flex items-center gap-3 shrink-0"> <div className="flex items-center gap-3 shrink-0">
<img src="/logo-icon.svg" alt="PragmaStack Logo" className="h-6 w-6" /> <Image
src="/logo-icon.svg"
alt="PragmaStack Logo"
width={24}
height={24}
className="h-6 w-6"
/>
<h1 className="text-base font-semibold">PragmaStack</h1> <h1 className="text-base font-semibold">PragmaStack</h1>
<Badge variant="secondary" className="text-xs"> <Badge variant="secondary" className="text-xs">
Dev Dev

View File

@@ -141,12 +141,12 @@ export function DemoCredentialsModal({ open, onClose }: DemoCredentialsModalProp
<DialogFooter> <DialogFooter>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 w-full"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-2 w-full">
<Button asChild variant="default" className="w-full"> <Button asChild variant="default" className="w-full">
<Link href="/login" onClick={onClose}> <Link href="/login?email=demo@example.com&password=Demo123!" onClick={onClose}>
Login as User Login as User
</Link> </Link>
</Button> </Button>
<Button asChild variant="default" className="w-full"> <Button asChild variant="default" className="w-full">
<Link href="/login" onClick={onClose}> <Link href="/login?email=admin@example.com&password=Admin123!" onClick={onClose}>
Login as Admin Login as Admin
</Link> </Link>
</Button> </Button>

View File

@@ -5,6 +5,7 @@
'use client'; 'use client';
import Image from 'next/image';
import { useState } from 'react'; import { useState } from 'react';
import { Link } from '@/lib/i18n/routing'; import { Link } from '@/lib/i18n/routing';
import { Menu, X, Github, Star } from 'lucide-react'; import { Menu, X, Github, Star } from 'lucide-react';
@@ -13,12 +14,20 @@ import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
import { LocaleSwitcher } from '@/components/i18n'; import { LocaleSwitcher } from '@/components/i18n';
import { ThemeToggle } from '@/components/theme'; import { ThemeToggle } from '@/components/theme';
import { useIsAuthenticated, useLogout } from '@/lib/api/hooks/useAuth';
interface HeaderProps { interface HeaderProps {
onOpenDemoModal: () => void; onOpenDemoModal: () => void;
} }
export function Header({ onOpenDemoModal }: HeaderProps) { export function Header({ onOpenDemoModal }: HeaderProps) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const isAuthenticated = useIsAuthenticated();
const logoutMutation = useLogout();
const handleLogout = () => {
logoutMutation.mutate();
};
const navLinks = [ const navLinks = [
{ href: '/', label: 'Home' }, { href: '/', label: 'Home' },
@@ -31,8 +40,17 @@ export function Header({ onOpenDemoModal }: HeaderProps) {
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"> <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-6"> <div className="container mx-auto flex h-16 items-center justify-between px-6">
{/* Logo */} {/* Logo */}
<Link href="/" className="flex items-center gap-2 font-bold text-xl hover:opacity-80 transition-opacity"> <Link
<img src="/logo-icon.svg" alt="PragmaStack Logo" className="h-8 w-8" /> href="/"
className="flex items-center gap-2 font-bold text-xl hover:opacity-80 transition-opacity"
>
<Image
src="/logo-icon.svg"
alt="PragmaStack Logo"
width={32}
height={32}
className="h-8 w-8"
/>
<span className="bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent"> <span className="bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
PragmaStack PragmaStack
</span> </span>
@@ -75,9 +93,16 @@ export function Header({ onOpenDemoModal }: HeaderProps) {
<Button onClick={onOpenDemoModal} variant="default" size="sm"> <Button onClick={onOpenDemoModal} variant="default" size="sm">
Try Demo Try Demo
</Button> </Button>
<Button asChild variant="outline" size="sm">
<Link href="/login">Login</Link> {isAuthenticated ? (
</Button> <Button onClick={handleLogout} variant="outline" size="sm">
Logout
</Button>
) : (
<Button asChild variant="outline" size="sm">
<Link href="/login">Login</Link>
</Button>
)}
</nav> </nav>
{/* Mobile Menu Toggle */} {/* Mobile Menu Toggle */}
@@ -138,11 +163,25 @@ export function Header({ onOpenDemoModal }: HeaderProps) {
> >
Try Demo Try Demo
</Button> </Button>
<Button asChild variant="outline" className="w-full">
<Link href="/login" onClick={() => setMobileMenuOpen(false)}> {isAuthenticated ? (
Login <Button
</Link> onClick={() => {
</Button> setMobileMenuOpen(false);
handleLogout();
}}
variant="outline"
className="w-full"
>
Logout
</Button>
) : (
<Button asChild variant="outline" className="w-full">
<Link href="/login" onClick={() => setMobileMenuOpen(false)}>
Login
</Link>
</Button>
)}
</div> </div>
</nav> </nav>
</SheetContent> </SheetContent>

View File

@@ -124,8 +124,6 @@ export function HeroSection({ onOpenDemoModal }: HeroSectionProps) {
</Link> </Link>
</Button> </Button>
</motion.div> </motion.div>
</div> </div>
</div> </div>
</section> </section>

View File

@@ -66,9 +66,7 @@ export function TechStackSection() {
viewport={{ once: true, margin: '-100px' }} viewport={{ once: true, margin: '-100px' }}
transition={{ duration: 0.6 }} transition={{ duration: 0.6 }}
> >
<h2 className="text-3xl md:text-4xl font-bold mb-4"> <h2 className="text-3xl md:text-4xl font-bold mb-4">A Stack You Can Trust</h2>
A Stack You Can Trust
</h2>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto"> <p className="text-lg text-muted-foreground max-w-2xl mx-auto">
We chose these tools because they are boring, reliable, and standard. No hype, just We chose these tools because they are boring, reliable, and standard. No hype, just
results. Async architecture, type safety, and developer experience. results. Async architecture, type safety, and developer experience.

View File

@@ -5,6 +5,7 @@
'use client'; 'use client';
import Image from 'next/image';
import { Link } from '@/lib/i18n/routing'; import { Link } from '@/lib/i18n/routing';
export function Footer() { export function Footer() {
@@ -15,7 +16,13 @@ export function Footer() {
<div className="container mx-auto px-4 py-6"> <div className="container mx-auto px-4 py-6">
<div className="flex flex-col items-center justify-between space-y-4 md:flex-row md:space-y-0"> <div className="flex flex-col items-center justify-between space-y-4 md:flex-row md:space-y-0">
<div className="flex items-center gap-2 text-center text-sm text-muted-foreground md:text-left"> <div className="flex items-center gap-2 text-center text-sm text-muted-foreground md:text-left">
<img src="/logo-icon.svg" alt="PragmaStack Logo" className="h-5 w-5 opacity-70" /> <Image
src="/logo-icon.svg"
alt="PragmaStack Logo"
width={20}
height={20}
className="h-5 w-5 opacity-70"
/>
<span>© {currentYear} PragmaStack. All rights reserved.</span> <span>© {currentYear} PragmaStack. All rights reserved.</span>
</div> </div>
<div className="flex space-x-6"> <div className="flex space-x-6">

View File

@@ -6,6 +6,7 @@
'use client'; 'use client';
import Image from 'next/image';
import { Link } from '@/lib/i18n/routing'; import { Link } from '@/lib/i18n/routing';
import { usePathname } from '@/lib/i18n/routing'; import { usePathname } from '@/lib/i18n/routing';
import { useAuth } from '@/lib/auth/AuthContext'; import { useAuth } from '@/lib/auth/AuthContext';
@@ -83,7 +84,13 @@ export function Header() {
{/* Logo */} {/* Logo */}
<div className="flex items-center space-x-8"> <div className="flex items-center space-x-8">
<Link href="/" className="flex items-center space-x-2"> <Link href="/" className="flex items-center space-x-2">
<img src="/logo-icon.svg" alt="PragmaStack Logo" className="h-8 w-8" /> <Image
src="/logo-icon.svg"
alt="PragmaStack Logo"
width={32}
height={32}
className="h-8 w-8"
/>
<span className="text-xl font-bold text-foreground">PragmaStack</span> <span className="text-xl font-bold text-foreground">PragmaStack</span>
</Link> </Link>

View File

@@ -9,7 +9,7 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/navigation'; import { useRouter } from '@/lib/i18n/routing';
import { import {
login, login,
register, register,