Temporarily disable middleware for debugging
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
124
.planning/REQUIREMENTS.md.bak
Normal file
124
.planning/REQUIREMENTS.md.bak
Normal file
@@ -0,0 +1,124 @@
|
||||
# Requirements: Leopost
|
||||
|
||||
**Defined:** 2026-01-30
|
||||
**Core Value:** L'AI fa il lavoro sporco del social media manager — minimo sforzo, massima resa
|
||||
|
||||
## v1 Requirements
|
||||
|
||||
Requirements per il rilascio iniziale. Ogni requirement mappa a fasi della roadmap.
|
||||
|
||||
### Authentication
|
||||
|
||||
- [ ] **AUTH-01**: Utente può registrarsi con email/password
|
||||
- [ ] **AUTH-02**: Utente può accedere con Google OAuth
|
||||
- [ ] **AUTH-03**: Sistema supporta 3 piani (Free, Creator, Pro) con limiti configurabili
|
||||
- [ ] **AUTH-04**: Utente può collegare account Facebook tramite OAuth
|
||||
|
||||
### Onboarding
|
||||
|
||||
- [ ] **ONBR-01**: Wizard raccoglie info base (nome attività, settore, descrizione)
|
||||
- [ ] **ONBR-02**: Contesto utente persiste tra sessioni (memoria)
|
||||
|
||||
### AI Chat
|
||||
|
||||
- [ ] **CHAT-01**: Interfaccia chat come esperienza principale dell'app
|
||||
- [ ] **CHAT-02**: Utente può scegliere modello AI (GPT, Claude, Gemini)
|
||||
- [ ] **CHAT-03**: AI propone azioni suggerite dopo completamento onboarding
|
||||
- [ ] **CHAT-04**: AI memorizza e impara preferenze utente nel tempo
|
||||
|
||||
### Content Generation
|
||||
|
||||
- [ ] **CONT-01**: AI genera post testuali su richiesta dell'utente
|
||||
- [ ] **CONT-02**: AI genera immagini per i post
|
||||
- [ ] **CONT-03**: AI crea piano editoriale (calendario contenuti suggeriti)
|
||||
|
||||
### Scheduling & Publishing
|
||||
|
||||
- [ ] **SCHD-01**: Utente può programmare post con data/ora specifica
|
||||
- [ ] **SCHD-02**: Post vengono pubblicati automaticamente su Facebook
|
||||
- [ ] **SCHD-03**: Utente può configurare livello automazione (da approval a autopilot)
|
||||
- [ ] **SCHD-04**: Calendario editoriale visuale mostra post programmati
|
||||
|
||||
### Messaging Integration
|
||||
|
||||
- [ ] **MSGN-01**: Utente può inviare comandi via Telegram bot
|
||||
- [ ] **MSGN-02**: Utente può inviare comandi via WhatsApp bot
|
||||
|
||||
## v2 Requirements
|
||||
|
||||
Deferred per release future. Tracciati ma non nella roadmap corrente.
|
||||
|
||||
### Authentication
|
||||
|
||||
- **AUTH-05**: Utente può accedere con Facebook OAuth
|
||||
- **AUTH-06**: Utente può accedere con LinkedIn OAuth
|
||||
- **AUTH-07**: Utente può accedere con Instagram OAuth
|
||||
|
||||
### Social Platforms
|
||||
|
||||
- **SOCL-01**: Utente può collegare e pubblicare su Instagram
|
||||
- **SOCL-02**: Utente può collegare e pubblicare su LinkedIn
|
||||
- **SOCL-03**: Utente può collegare e pubblicare su YouTube
|
||||
- **SOCL-04**: Utente può collegare e pubblicare su TikTok
|
||||
- **SOCL-05**: Utente può collegare e pubblicare su X (Twitter)
|
||||
|
||||
### Onboarding Extended
|
||||
|
||||
- **ONBR-03**: Wizard raccoglie brand kit (logo, colori, font)
|
||||
- **ONBR-04**: Wizard raccoglie siti web e link
|
||||
- **ONBR-05**: Wizard raccoglie target audience e zona geografica
|
||||
|
||||
### Content Generation Extended
|
||||
|
||||
- **CONT-04**: AI adatta tono/lunghezza/hashtag per piattaforma specifica
|
||||
- **CONT-05**: Utente può fornire template grafici come base
|
||||
- **CONT-06**: AI genera video (oltre a immagini)
|
||||
|
||||
## Out of Scope
|
||||
|
||||
Esclusi esplicitamente. Documentati per prevenire scope creep.
|
||||
|
||||
| Feature | Reason |
|
||||
|---------|--------|
|
||||
| App native iOS/Android | Architettura headless pronta, ma web-first per v1 |
|
||||
| Dashboard analytics avanzate | Focus su creazione/pubblicazione, non analytics |
|
||||
| Social listening | Complessità elevata, non core per freelance |
|
||||
| Influencer marketplace | Fuori target (freelance, non brand) |
|
||||
| White-label | Enterprise feature, non micro-SaaS |
|
||||
| Video editing integrato | Complessità, costi storage/bandwidth |
|
||||
| Gestione ads/campagne | Fuori scope, focus su contenuti organici |
|
||||
|
||||
## Traceability
|
||||
|
||||
Quali fasi coprono quali requirements. Aggiornato durante creazione roadmap.
|
||||
|
||||
| Requirement | Phase | Status |
|
||||
|-------------|-------|--------|
|
||||
| AUTH-01 | TBD | Pending |
|
||||
| AUTH-02 | TBD | Pending |
|
||||
| AUTH-03 | TBD | Pending |
|
||||
| AUTH-04 | TBD | Pending |
|
||||
| ONBR-01 | TBD | Pending |
|
||||
| ONBR-02 | TBD | Pending |
|
||||
| CHAT-01 | TBD | Pending |
|
||||
| CHAT-02 | TBD | Pending |
|
||||
| CHAT-03 | TBD | Pending |
|
||||
| CHAT-04 | TBD | Pending |
|
||||
| CONT-01 | TBD | Pending |
|
||||
| CONT-02 | TBD | Pending |
|
||||
| CONT-03 | TBD | Pending |
|
||||
| SCHD-01 | TBD | Pending |
|
||||
| SCHD-02 | TBD | Pending |
|
||||
| SCHD-03 | TBD | Pending |
|
||||
| SCHD-04 | TBD | Pending |
|
||||
| MSGN-01 | TBD | Pending |
|
||||
| MSGN-02 | TBD | Pending |
|
||||
|
||||
**Coverage:**
|
||||
- v1 requirements: 19 total
|
||||
- Mapped to phases: 0
|
||||
- Unmapped: 19 ⚠️
|
||||
|
||||
---
|
||||
*Requirements defined: 2026-01-30*
|
||||
*Last updated: 2026-01-30 after initial definition*
|
||||
38
README.md
Normal file
38
README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Leopost
|
||||
|
||||
Lab project hosted at https://lab.mlhub.it/leopost/
|
||||
|
||||
## Development
|
||||
|
||||
This project uses [gsd (get-shit-done)](https://github.com/glittercowboy/get-shit-done) for spec-driven development.
|
||||
|
||||
### Getting Started
|
||||
|
||||
1. Run `/gsd:new-project` to initialize project specs
|
||||
2. Follow gsd workflow: discuss → plan → execute → verify
|
||||
3. Run `vps-lab-deploy` when ready to deploy
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
leopost/
|
||||
├── .planning/ # gsd planning docs (created by /gsd:new-project)
|
||||
│ ├── PROJECT.md # Project vision
|
||||
│ ├── REQUIREMENTS.md # Scoped requirements
|
||||
│ ├── ROADMAP.md # Phase breakdown
|
||||
│ └── STATE.md # Project memory
|
||||
├── src/ # Source code (created during development)
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Repository
|
||||
|
||||
- **Gitea**: https://git.mlhub.it/Michele/leopost
|
||||
- **Clone**: `git clone https://git.mlhub.it/Michele/leopost.git`
|
||||
|
||||
## Deployment
|
||||
|
||||
When ready to deploy, use `vps-lab-deploy` skill.
|
||||
|
||||
- **URL**: https://lab.mlhub.it/leopost/
|
||||
- **VPS**: /opt/lab-leopost/
|
||||
97
src/app/(auth)/reset-password/page.tsx
Normal file
97
src/app/(auth)/reset-password/page.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
'use client'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { createClient } from '@/lib/supabase/client'
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [success, setSuccess] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const supabase = createClient()
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setError(null)
|
||||
setLoading(true)
|
||||
|
||||
const { error: resetError } = await supabase.auth.resetPasswordForEmail(
|
||||
email,
|
||||
{
|
||||
redirectTo: `${window.location.origin}/auth/callback?next=/update-password`,
|
||||
}
|
||||
)
|
||||
|
||||
if (resetError) {
|
||||
setError(resetError.message)
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setSuccess(true)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle>Recupera password</CardTitle>
|
||||
<CardDescription>
|
||||
Inserisci la tua email per ricevere il link di reset
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{success ? (
|
||||
<div className="text-center space-y-4">
|
||||
<div className="p-4 bg-green-50 border border-green-200 rounded-md">
|
||||
<p className="text-green-800">
|
||||
Se l'email esiste, riceverai un link per reimpostare la password.
|
||||
</p>
|
||||
</div>
|
||||
<Link href="/login" className="text-blue-600 hover:underline">
|
||||
Torna al login
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{error && (
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded-md">
|
||||
<p className="text-red-800 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email
|
||||
</label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
placeholder="tu@esempio.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading ? 'Invio...' : 'Invia link di reset'}
|
||||
</Button>
|
||||
|
||||
<p className="text-center text-sm text-gray-600">
|
||||
<Link href="/login" className="text-blue-600 hover:underline">
|
||||
Torna al login
|
||||
</Link>
|
||||
</p>
|
||||
</form>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
144
src/app/(auth)/update-password/page.tsx
Normal file
144
src/app/(auth)/update-password/page.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
'use client'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { createClient } from '@/lib/supabase/client'
|
||||
|
||||
export default function UpdatePasswordPage() {
|
||||
const [password, setPassword] = useState('')
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({})
|
||||
const [success, setSuccess] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const supabase = createClient()
|
||||
|
||||
function validatePassword(pwd: string): string | null {
|
||||
if (pwd.length < 8) {
|
||||
return 'La password deve contenere almeno 8 caratteri'
|
||||
}
|
||||
if (!/[0-9]/.test(pwd)) {
|
||||
return 'La password deve contenere almeno un numero'
|
||||
}
|
||||
if (!/[A-Z]/.test(pwd)) {
|
||||
return 'La password deve contenere almeno una lettera maiuscola'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setError(null)
|
||||
setFieldErrors({})
|
||||
setLoading(true)
|
||||
|
||||
// Validate password
|
||||
const passwordError = validatePassword(password)
|
||||
if (passwordError) {
|
||||
setFieldErrors({ password: passwordError })
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate confirm password
|
||||
if (password !== confirmPassword) {
|
||||
setFieldErrors({ confirmPassword: 'Le password non coincidono' })
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const { error: updateError } = await supabase.auth.updateUser({
|
||||
password: password,
|
||||
})
|
||||
|
||||
if (updateError) {
|
||||
setError(updateError.message)
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setSuccess(true)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle>Nuova password</CardTitle>
|
||||
<CardDescription>
|
||||
Inserisci la tua nuova password
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{success ? (
|
||||
<div className="text-center space-y-4">
|
||||
<div className="p-4 bg-green-50 border border-green-200 rounded-md">
|
||||
<p className="text-green-800">Password aggiornata con successo!</p>
|
||||
</div>
|
||||
<Link href="/login" className="text-blue-600 hover:underline">
|
||||
Vai al login
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{error && (
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded-md">
|
||||
<p className="text-red-800 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Nuova Password
|
||||
</label>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
placeholder="Minimo 8 caratteri"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
error={!!fieldErrors.password}
|
||||
/>
|
||||
{fieldErrors.password && (
|
||||
<p className="mt-1 text-sm text-red-600">{fieldErrors.password}</p>
|
||||
)}
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Almeno 8 caratteri, 1 numero, 1 maiuscola
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Conferma Password
|
||||
</label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
placeholder="Ripeti la password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
error={!!fieldErrors.confirmPassword}
|
||||
/>
|
||||
{fieldErrors.confirmPassword && (
|
||||
<p className="mt-1 text-sm text-red-600">{fieldErrors.confirmPassword}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading ? 'Aggiornamento...' : 'Aggiorna password'}
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
26
src/app/(auth)/verify-email/page.tsx
Normal file
26
src/app/(auth)/verify-email/page.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function VerifyEmailPage() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle>Verifica la tua email</CardTitle>
|
||||
<CardDescription>
|
||||
Ti abbiamo inviato un link di conferma
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-center space-y-4">
|
||||
<p className="text-gray-600">
|
||||
Controlla la tua casella email e clicca sul link per attivare il tuo account.
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Non hai ricevuto l'email? Controlla lo spam.
|
||||
</p>
|
||||
<Link href="/login" className="text-blue-600 hover:underline">
|
||||
Torna al login
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
1
tsconfig.tsbuildinfo
Normal file
1
tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user