Compare commits

...

6 Commits

Author SHA1 Message Date
Felipe Cardoso
a410586cfb 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.
2025-11-21 07:42:40 +01:00
Felipe Cardoso
0e34cab921 Add logs and logs-dev targets to Makefile for streamlined log access 2025-11-21 07:32:11 +01:00
Felipe Cardoso
3cf3858fca Update Makefile to refine clean-slate target with explicit dev compose file and orphan removal 2025-11-21 07:25:22 +01:00
Felipe Cardoso
db0c555041 Add ThemeToggle to Header component
- Integrated `ThemeToggle` for light/dark mode functionality in both desktop and mobile views.
- Adjusted layout styles to accommodate new control next to `LocaleSwitcher` with consistent spacing.
2025-11-20 15:16:49 +01:00
Felipe Cardoso
51ad80071a Ensure virtualenv binaries are on PATH in entrypoint script for consistent command execution 2025-11-20 15:16:30 +01:00
Felipe Cardoso
d730ab7526 Update .dockerignore, alembic revision, and entrypoint script for consistency and reliability
- Expanded `.dockerignore` to exclude Python and packaging-related artifacts for cleaner Docker builds.
- Updated Alembic `down_revision` in migration script to reflect correct dependency chain.
- Modified entrypoint script to use `uv` with `--no-project` flag, preventing permission issues in bind-mounted volumes.
2025-11-20 15:12:55 +01:00
19 changed files with 184 additions and 34 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

@@ -1,4 +1,4 @@
.PHONY: dev dev-full prod down clean clean-slate .PHONY: dev dev-full prod down logs logs-dev clean clean-slate
VERSION ?= latest VERSION ?= latest
REGISTRY := gitea.pragmazest.com/cardosofelipe/app REGISTRY := gitea.pragmazest.com/cardosofelipe/app
@@ -22,6 +22,12 @@ prod:
down: down:
docker compose down docker compose down
logs:
docker compose logs -f
logs-dev:
docker compose -f docker-compose.dev.yml logs -f
deploy: deploy:
docker compose -f docker-compose.deploy.yml pull docker compose -f docker-compose.deploy.yml pull
docker compose -f docker-compose.deploy.yml up -d docker compose -f docker-compose.deploy.yml up -d
@@ -31,7 +37,7 @@ clean:
# WARNING! THIS REMOVES CONTAINERS AND VOLUMES AS WELL - DO NOT USE THIS UNLESS YOU WANT TO START OVER WITH DATA AND ALL # WARNING! THIS REMOVES CONTAINERS AND VOLUMES AS WELL - DO NOT USE THIS UNLESS YOU WANT TO START OVER WITH DATA AND ALL
clean-slate: clean-slate:
docker compose down -v docker compose -f docker-compose.dev.yml down -v --remove-orphans
push-images: push-images:
docker build -t $(REGISTRY)/backend:$(VERSION) ./backend docker build -t $(REGISTRY)/backend:$(VERSION) ./backend

View File

@@ -1,6 +1,6 @@
# PragmaStack # PragmaStack
<div align="center"> <div style="text-align: center">
<img src="frontend/public/logo.svg" alt="PragmaStack Logo" width="200" /> <img src="frontend/public/logo.svg" alt="PragmaStack Logo" width="200" />
<br /> <br />
</div> </div>

View File

@@ -1,2 +1,17 @@
.venv .venv
*.iml *.iml
# Python build and cache artifacts
__pycache__/
.pytest_cache/
.mypy_cache/
.ruff_cache/
*.pyc
*.pyo
# Packaging artifacts
*.egg-info/
build/
dist/
htmlcov/
.uv_cache/

View File

@@ -1,7 +1,7 @@
"""add user locale preference column """add user locale preference column
Revision ID: c8e9f3a2d1b4 Revision ID: c8e9f3a2d1b4
Revises: b76c725fc3cf Revises: 1174fffbe3e4
Create Date: 2025-11-17 18:00:00.000000 Create Date: 2025-11-17 18:00:00.000000
""" """
@@ -13,7 +13,7 @@ from alembic import op
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = "c8e9f3a2d1b4" revision: str = "c8e9f3a2d1b4"
down_revision: str | None = "b76c725fc3cf" down_revision: str | None = "1174fffbe3e4"
branch_labels: str | Sequence[str] | None = None branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None

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")

14
backend/entrypoint.sh Normal file → Executable file
View File

@@ -2,11 +2,21 @@
set -e set -e
echo "Starting Backend" echo "Starting Backend"
# Ensure the project's virtualenv binaries are on PATH so commands like
# 'uvicorn' work even when not prefixed by 'uv run'. This matches how uv
# installs the env into /app/.venv in our containers.
if [ -d "/app/.venv/bin" ]; then
export PATH="/app/.venv/bin:$PATH"
fi
# Apply database migrations # Apply database migrations
uv run alembic upgrade head # Avoid installing the project in editable mode (which tries to write egg-info)
# when running inside a bind-mounted volume with restricted permissions.
# See: https://github.com/astral-sh/uv (use --no-project to skip project build)
uv run --no-project alembic upgrade head
# Initialize database (creates first superuser if needed) # Initialize database (creates first superuser if needed)
uv run python app/init_db.py uv run --no-project python app/init_db.py
# Execute the command passed to docker run # Execute the command passed to docker run
exec "$@" exec "$@"

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,12 +5,16 @@
'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';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'; import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
import { LocaleSwitcher } from '@/components/i18n'; import { LocaleSwitcher } from '@/components/i18n';
import { ThemeToggle } from '@/components/theme';
import { useIsAuthenticated, useLogout } from '@/lib/api/hooks/useAuth';
interface HeaderProps { interface HeaderProps {
onOpenDemoModal: () => void; onOpenDemoModal: () => void;
@@ -18,6 +22,12 @@ interface HeaderProps {
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' },
@@ -30,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>
@@ -67,13 +86,23 @@ export function Header({ onOpenDemoModal }: HeaderProps) {
{/* Locale Switcher */} {/* Locale Switcher */}
<LocaleSwitcher /> <LocaleSwitcher />
{/* Theme Toggle */}
<ThemeToggle />
{/* CTAs */} {/* CTAs */}
<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 */}
@@ -118,9 +147,10 @@ export function Header({ onOpenDemoModal }: HeaderProps) {
</a> </a>
<div className="border-t pt-4 mt-4 space-y-3"> <div className="border-t pt-4 mt-4 space-y-3">
{/* Locale Switcher */} {/* Locale Switcher & Theme Toggle */}
<div className="flex justify-center"> <div className="flex justify-center gap-4">
<LocaleSwitcher /> <LocaleSwitcher />
<ThemeToggle />
</div> </div>
<Button <Button
@@ -133,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,