From 036aadb09ab5c7a88de0d9106a3bd1e95fbf43f5 Mon Sep 17 00:00:00 2001 From: fabio Date: Sun, 22 Feb 2026 17:47:28 +0100 Subject: [PATCH] prompt 6 --- codex-prompt/prompt-6.txt | 27 ++ codex-prompt/prompt-7.txt | 0 internal/app/app.go | 4 +- internal/controllers/auth_controller.go | 196 ++++++++++++++ internal/controllers/render.go | 53 ++++ internal/http/router.go | 40 +-- .../repo/email_verification_token_repo.go | 42 +++ internal/repo/password_reset_token_repo.go | 42 +++ internal/repo/user_repo.go | 28 ++ internal/services/auth_service.go | 247 ++++++++++++++++++ web/templates/layout.html | 42 ++- web/templates/public/_flash.html | 6 + web/templates/public/forgot_password.html | 9 + web/templates/public/home.html | 9 + web/templates/public/login.html | 11 + web/templates/public/reset_password.html | 12 + web/templates/public/signup.html | 11 + web/templates/public/verify_notice.html | 6 + 18 files changed, 746 insertions(+), 39 deletions(-) create mode 100644 codex-prompt/prompt-7.txt create mode 100644 internal/controllers/auth_controller.go create mode 100644 internal/controllers/render.go create mode 100644 internal/repo/email_verification_token_repo.go create mode 100644 internal/repo/password_reset_token_repo.go create mode 100644 internal/services/auth_service.go create mode 100644 web/templates/public/_flash.html create mode 100644 web/templates/public/forgot_password.html create mode 100644 web/templates/public/home.html create mode 100644 web/templates/public/login.html create mode 100644 web/templates/public/reset_password.html create mode 100644 web/templates/public/signup.html create mode 100644 web/templates/public/verify_notice.html diff --git a/codex-prompt/prompt-6.txt b/codex-prompt/prompt-6.txt index e69de29..b356f38 100644 --- a/codex-prompt/prompt-6.txt +++ b/codex-prompt/prompt-6.txt @@ -0,0 +1,27 @@ +Implementa AUTH completo (server-rendered) e templates in /web/templates/public. + +Routes: +- GET/POST /signup +- GET/POST /login +- POST /logout +- GET /verify-email?token=... +- GET/POST /forgot-password +- GET/POST /reset-password?token=... + +Comportamento: +- Signup crea user (role=user, verified=false), genera verify token (hash in DB), invia email (mailer). +- Login: blocca se email non verificata. +- Verify-email: valida token, set EmailVerified=true, elimina token. +- Forgot-password: risposta sempre generica; se user esiste+verified, genera reset token e invia email. +- Reset-password: valida token, aggiorna password, elimina token. + +Crea templates: +- public/login.html +- public/signup.html +- public/forgot_password.html +- public/reset_password.html +- public/verify_notice.html +- public/home.html (opzionale) +Aggiungi partial per flash (public/_flash.html) e includilo nel layout. + +Usa repo/service per accesso DB e logica (non tutto nel controller). \ No newline at end of file diff --git a/codex-prompt/prompt-7.txt b/codex-prompt/prompt-7.txt new file mode 100644 index 0000000..e69de29 diff --git a/internal/app/app.go b/internal/app/app.go index ea27d24..5aa0448 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -51,7 +51,9 @@ func NewApp(cfg *config.Config) (*fiber.App, error) { } } - apphttp.RegisterRoutes(app, store, database, cfg) + if err := apphttp.RegisterRoutes(app, store, database, cfg); err != nil { + return nil, fmt.Errorf("register routes: %w", err) + } return app, nil } diff --git a/internal/controllers/auth_controller.go b/internal/controllers/auth_controller.go new file mode 100644 index 0000000..0140c89 --- /dev/null +++ b/internal/controllers/auth_controller.go @@ -0,0 +1,196 @@ +package controllers + +import ( + "errors" + "strings" + + httpmw "trustcontact/internal/http/middleware" + "trustcontact/internal/services" + + "github.com/gofiber/fiber/v2" +) + +type AuthController struct { + authService *services.AuthService +} + +func NewAuthController(authService *services.AuthService) *AuthController { + return &AuthController{authService: authService} +} + +func (ac *AuthController) ShowHome(c *fiber.Ctx) error { + return renderPublic(c, "home.html", map[string]any{ + "Title": "Home", + "NavSection": "public", + }) +} + +func (ac *AuthController) ShowSignup(c *fiber.Ctx) error { + return renderPublic(c, "signup.html", map[string]any{ + "Title": "Sign up", + "NavSection": "public", + }) +} + +func (ac *AuthController) Signup(c *fiber.Ctx) error { + email := strings.TrimSpace(c.FormValue("email")) + password := c.FormValue("password") + + if err := ac.authService.Signup(c.UserContext(), email, password); err != nil { + if errors.Is(err, services.ErrEmailAlreadyExists) { + httpmw.SetTemplateData(c, "FlashError", "Email gia registrata") + } else { + httpmw.SetTemplateData(c, "FlashError", "Impossibile completare la registrazione") + } + return renderPublic(c, "signup.html", map[string]any{ + "Title": "Sign up", + "NavSection": "public", + "Email": email, + }) + } + + if err := httpmw.SetFlashSuccess(c, "Registrazione completata. Controlla la tua email per verificare l'account."); err != nil { + return err + } + return c.Redirect("/verify-notice") +} + +func (ac *AuthController) ShowLogin(c *fiber.Ctx) error { + return renderPublic(c, "login.html", map[string]any{ + "Title": "Login", + "NavSection": "public", + }) +} + +func (ac *AuthController) Login(c *fiber.Ctx) error { + email := strings.TrimSpace(c.FormValue("email")) + password := c.FormValue("password") + + user, err := ac.authService.Login(email, password) + if err != nil { + switch { + case errors.Is(err, services.ErrEmailNotVerified): + httpmw.SetTemplateData(c, "FlashError", "Email non verificata. Controlla la posta.") + case errors.Is(err, services.ErrInvalidCredentials): + httpmw.SetTemplateData(c, "FlashError", "Credenziali non valide") + default: + httpmw.SetTemplateData(c, "FlashError", "Errore durante il login") + } + + return renderPublic(c, "login.html", map[string]any{ + "Title": "Login", + "NavSection": "public", + "Email": email, + }) + } + + if err := httpmw.SetSessionUserID(c, user.ID); err != nil { + return err + } + if err := httpmw.SetFlashSuccess(c, "Login effettuato"); err != nil { + return err + } + return c.Redirect("/private") +} + +func (ac *AuthController) Logout(c *fiber.Ctx) error { + if err := httpmw.ClearSessionUser(c); err != nil { + return err + } + if err := httpmw.SetFlashSuccess(c, "Logout effettuato"); err != nil { + return err + } + return c.Redirect("/login") +} + +func (ac *AuthController) VerifyEmail(c *fiber.Ctx) error { + token := strings.TrimSpace(c.Query("token")) + if token == "" { + httpmw.SetTemplateData(c, "FlashError", "Token non valido") + return renderPublic(c, "verify_notice.html", map[string]any{ + "Title": "Verifica email", + "NavSection": "public", + }) + } + + if err := ac.authService.VerifyEmail(token); err != nil { + httpmw.SetTemplateData(c, "FlashError", "Token non valido o scaduto") + return renderPublic(c, "verify_notice.html", map[string]any{ + "Title": "Verifica email", + "NavSection": "public", + }) + } + + if err := httpmw.SetFlashSuccess(c, "Email verificata. Ora puoi accedere."); err != nil { + return err + } + return c.Redirect("/login") +} + +func (ac *AuthController) ShowVerifyNotice(c *fiber.Ctx) error { + return renderPublic(c, "verify_notice.html", map[string]any{ + "Title": "Verifica email", + "NavSection": "public", + }) +} + +func (ac *AuthController) ShowForgotPassword(c *fiber.Ctx) error { + return renderPublic(c, "forgot_password.html", map[string]any{ + "Title": "Forgot password", + "NavSection": "public", + }) +} + +func (ac *AuthController) ForgotPassword(c *fiber.Ctx) error { + email := strings.TrimSpace(c.FormValue("email")) + if err := ac.authService.ForgotPassword(c.UserContext(), email); err != nil { + httpmw.SetTemplateData(c, "FlashError", "Impossibile elaborare la richiesta") + return renderPublic(c, "forgot_password.html", map[string]any{ + "Title": "Forgot password", + "NavSection": "public", + "Email": email, + }) + } + + httpmw.SetTemplateData(c, "FlashSuccess", "Se l'account esiste, riceverai una email con le istruzioni.") + return renderPublic(c, "forgot_password.html", map[string]any{ + "Title": "Forgot password", + "NavSection": "public", + }) +} + +func (ac *AuthController) ShowResetPassword(c *fiber.Ctx) error { + token := strings.TrimSpace(c.Query("token")) + return renderPublic(c, "reset_password.html", map[string]any{ + "Title": "Reset password", + "NavSection": "public", + "Token": token, + }) +} + +func (ac *AuthController) ResetPassword(c *fiber.Ctx) error { + token := strings.TrimSpace(c.Query("token")) + password := c.FormValue("password") + + if token == "" { + httpmw.SetTemplateData(c, "FlashError", "Token non valido") + return renderPublic(c, "reset_password.html", map[string]any{ + "Title": "Reset password", + "NavSection": "public", + }) + } + + if err := ac.authService.ResetPassword(token, password); err != nil { + httpmw.SetTemplateData(c, "FlashError", "Token non valido o scaduto") + return renderPublic(c, "reset_password.html", map[string]any{ + "Title": "Reset password", + "NavSection": "public", + "Token": token, + }) + } + + if err := httpmw.SetFlashSuccess(c, "Password aggiornata. Effettua il login."); err != nil { + return err + } + return c.Redirect("/login") +} diff --git a/internal/controllers/render.go b/internal/controllers/render.go new file mode 100644 index 0000000..69d4a8e --- /dev/null +++ b/internal/controllers/render.go @@ -0,0 +1,53 @@ +package controllers + +import ( + "bytes" + "html/template" + "path/filepath" + + "github.com/gofiber/fiber/v2" +) + +func renderPublic(c *fiber.Ctx, page string, data map[string]any) error { + viewData := map[string]any{} + for k, v := range localsTemplateData(c) { + viewData[k] = v + } + for k, v := range data { + viewData[k] = v + } + + if _, ok := viewData["Title"]; !ok { + viewData["Title"] = "Trustcontact" + } + if _, ok := viewData["NavSection"]; !ok { + viewData["NavSection"] = "public" + } + + files := []string{ + "web/templates/layout.html", + "web/templates/public/_flash.html", + filepath.Join("web/templates/public", page), + } + + tmpl, err := template.ParseFiles(files...) + if err != nil { + return err + } + + var out bytes.Buffer + if err := tmpl.ExecuteTemplate(&out, "layout.html", viewData); err != nil { + return err + } + + c.Type("html", "utf-8") + return c.Send(out.Bytes()) +} + +func localsTemplateData(c *fiber.Ctx) map[string]any { + data, ok := c.Locals("template_data").(map[string]any) + if !ok || data == nil { + return map[string]any{} + } + return data +} diff --git a/internal/http/router.go b/internal/http/router.go index 307fd6d..4f5052d 100644 --- a/internal/http/router.go +++ b/internal/http/router.go @@ -1,46 +1,54 @@ package http import ( + "fmt" "trustcontact/internal/config" + "trustcontact/internal/controllers" httpmw "trustcontact/internal/http/middleware" + "trustcontact/internal/services" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/session" "gorm.io/gorm" ) -func RegisterRoutes(app *fiber.App, store *session.Store, database *gorm.DB, _ *config.Config) { +func RegisterRoutes(app *fiber.App, store *session.Store, database *gorm.DB, cfg *config.Config) error { app.Use(httpmw.SessionStoreMiddleware(store)) app.Use(httpmw.CurrentUserMiddleware(store, database)) app.Use(httpmw.ConsumeFlash()) + authService, err := services.NewAuthService(database, cfg) + if err != nil { + return fmt.Errorf("init auth service: %w", err) + } + authController := controllers.NewAuthController(authService) + app.Get("/healthz", func(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) }) - app.Get("/", func(c *fiber.Ctx) error { - c.Locals("nav_section", "public") - httpmw.SetTemplateData(c, "NavSection", "public") - return c.SendString("public area") - }) - - app.Get("/login", func(c *fiber.Ctx) error { - c.Locals("nav_section", "public") - httpmw.SetTemplateData(c, "NavSection", "public") - return c.SendString("login page") - }) + app.Get("/", authController.ShowHome) + app.Get("/signup", authController.ShowSignup) + app.Post("/signup", authController.Signup) + app.Get("/login", authController.ShowLogin) + app.Post("/login", authController.Login) + app.Post("/logout", authController.Logout) + app.Get("/verify-email", authController.VerifyEmail) + app.Get("/verify-notice", authController.ShowVerifyNotice) + app.Get("/forgot-password", authController.ShowForgotPassword) + app.Post("/forgot-password", authController.ForgotPassword) + app.Get("/reset-password", authController.ShowResetPassword) + app.Post("/reset-password", authController.ResetPassword) private := app.Group("/private", httpmw.RequireAuth()) private.Get("/", func(c *fiber.Ctx) error { - c.Locals("nav_section", "private") - httpmw.SetTemplateData(c, "NavSection", "private") return c.SendString("private area") }) admin := app.Group("/admin", httpmw.RequireAuth(), httpmw.RequireAdmin()) admin.Get("/", func(c *fiber.Ctx) error { - c.Locals("nav_section", "admin") - httpmw.SetTemplateData(c, "NavSection", "admin") return c.SendString("admin area") }) + + return nil } diff --git a/internal/repo/email_verification_token_repo.go b/internal/repo/email_verification_token_repo.go new file mode 100644 index 0000000..e59acc8 --- /dev/null +++ b/internal/repo/email_verification_token_repo.go @@ -0,0 +1,42 @@ +package repo + +import ( + "errors" + "time" + + "trustcontact/internal/models" + + "gorm.io/gorm" +) + +type EmailVerificationTokenRepo struct { + db *gorm.DB +} + +func NewEmailVerificationTokenRepo(db *gorm.DB) *EmailVerificationTokenRepo { + return &EmailVerificationTokenRepo{db: db} +} + +func (r *EmailVerificationTokenRepo) Create(token *models.EmailVerificationToken) error { + return r.db.Create(token).Error +} + +func (r *EmailVerificationTokenRepo) FindValidByHash(tokenHash string, now time.Time) (*models.EmailVerificationToken, error) { + var token models.EmailVerificationToken + err := r.db.Where("token_hash = ? AND expires_at > ?", tokenHash, now).First(&token).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + return &token, nil +} + +func (r *EmailVerificationTokenRepo) DeleteByID(id uint) error { + return r.db.Delete(&models.EmailVerificationToken{}, id).Error +} + +func (r *EmailVerificationTokenRepo) DeleteByUserID(userID uint) error { + return r.db.Where("user_id = ?", userID).Delete(&models.EmailVerificationToken{}).Error +} diff --git a/internal/repo/password_reset_token_repo.go b/internal/repo/password_reset_token_repo.go new file mode 100644 index 0000000..badb41f --- /dev/null +++ b/internal/repo/password_reset_token_repo.go @@ -0,0 +1,42 @@ +package repo + +import ( + "errors" + "time" + + "trustcontact/internal/models" + + "gorm.io/gorm" +) + +type PasswordResetTokenRepo struct { + db *gorm.DB +} + +func NewPasswordResetTokenRepo(db *gorm.DB) *PasswordResetTokenRepo { + return &PasswordResetTokenRepo{db: db} +} + +func (r *PasswordResetTokenRepo) Create(token *models.PasswordResetToken) error { + return r.db.Create(token).Error +} + +func (r *PasswordResetTokenRepo) FindValidByHash(tokenHash string, now time.Time) (*models.PasswordResetToken, error) { + var token models.PasswordResetToken + err := r.db.Where("token_hash = ? AND expires_at > ?", tokenHash, now).First(&token).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + return &token, nil +} + +func (r *PasswordResetTokenRepo) DeleteByID(id uint) error { + return r.db.Delete(&models.PasswordResetToken{}, id).Error +} + +func (r *PasswordResetTokenRepo) DeleteByUserID(userID uint) error { + return r.db.Where("user_id = ?", userID).Delete(&models.PasswordResetToken{}).Error +} diff --git a/internal/repo/user_repo.go b/internal/repo/user_repo.go index d2ee672..310ce92 100644 --- a/internal/repo/user_repo.go +++ b/internal/repo/user_repo.go @@ -27,3 +27,31 @@ func (r *UserRepo) FindByID(id uint) (*models.User, error) { return &user, nil } + +func (r *UserRepo) FindByEmail(email string) (*models.User, error) { + var user models.User + if err := r.db.Where("email = ?", email).First(&user).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + + return &user, nil +} + +func (r *UserRepo) Create(user *models.User) error { + return r.db.Create(user).Error +} + +func (r *UserRepo) SetEmailVerified(userID uint, verified bool) error { + return r.db.Model(&models.User{}). + Where("id = ?", userID). + Update("email_verified", verified).Error +} + +func (r *UserRepo) UpdatePasswordHash(userID uint, passwordHash string) error { + return r.db.Model(&models.User{}). + Where("id = ?", userID). + Update("password_hash", passwordHash).Error +} diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go new file mode 100644 index 0000000..2e7ee92 --- /dev/null +++ b/internal/services/auth_service.go @@ -0,0 +1,247 @@ +package services + +import ( + "context" + "errors" + "fmt" + "net/url" + "strings" + "time" + + "trustcontact/internal/auth" + "trustcontact/internal/config" + "trustcontact/internal/mailer" + "trustcontact/internal/models" + "trustcontact/internal/repo" + + "gorm.io/gorm" +) + +var ( + ErrEmailAlreadyExists = errors.New("email already exists") + ErrInvalidCredentials = errors.New("invalid credentials") + ErrEmailNotVerified = errors.New("email not verified") + ErrInvalidOrExpiredToken = errors.New("invalid or expired token") +) + +type AuthService struct { + cfg *config.Config + users *repo.UserRepo + verifyTokens *repo.EmailVerificationTokenRepo + resetTokens *repo.PasswordResetTokenRepo + mailer mailer.Mailer + templateRender *mailer.TemplateRenderer + nowFn func() time.Time +} + +func NewAuthService(database *gorm.DB, cfg *config.Config) (*AuthService, error) { + sender, err := mailer.NewMailer(cfg) + if err != nil { + return nil, err + } + + return &AuthService{ + cfg: cfg, + users: repo.NewUserRepo(database), + verifyTokens: repo.NewEmailVerificationTokenRepo(database), + resetTokens: repo.NewPasswordResetTokenRepo(database), + mailer: sender, + templateRender: mailer.NewTemplateRenderer(""), + nowFn: func() time.Time { + return time.Now().UTC() + }, + }, nil +} + +func (s *AuthService) Signup(ctx context.Context, email, password string) error { + email = normalizeEmail(email) + if email == "" || strings.TrimSpace(password) == "" { + return ErrInvalidCredentials + } + + existing, err := s.users.FindByEmail(email) + if err != nil { + return err + } + if existing != nil { + return ErrEmailAlreadyExists + } + + passwordHash, err := auth.HashPassword(password) + if err != nil { + return err + } + + user := &models.User{ + Email: email, + PasswordHash: passwordHash, + EmailVerified: false, + Role: models.RoleUser, + } + if err := s.users.Create(user); err != nil { + return err + } + + return s.issueVerifyEmail(ctx, user) +} + +func (s *AuthService) Login(email, password string) (*models.User, error) { + email = normalizeEmail(email) + user, err := s.users.FindByEmail(email) + if err != nil { + return nil, err + } + if user == nil { + return nil, ErrInvalidCredentials + } + + ok, err := auth.ComparePassword(user.PasswordHash, password) + if err != nil { + return nil, err + } + if !ok { + return nil, ErrInvalidCredentials + } + + if !user.EmailVerified { + return nil, ErrEmailNotVerified + } + + return user, nil +} + +func (s *AuthService) VerifyEmail(token string) error { + hash := auth.HashToken(token) + record, err := s.verifyTokens.FindValidByHash(hash, s.nowFn()) + if err != nil { + return err + } + if record == nil { + return ErrInvalidOrExpiredToken + } + + if err := s.users.SetEmailVerified(record.UserID, true); err != nil { + return err + } + + return s.verifyTokens.DeleteByID(record.ID) +} + +func (s *AuthService) ForgotPassword(ctx context.Context, email string) error { + email = normalizeEmail(email) + if email == "" { + return nil + } + + user, err := s.users.FindByEmail(email) + if err != nil { + return err + } + if user == nil || !user.EmailVerified { + return nil + } + + plainToken, err := auth.NewToken() + if err != nil { + return err + } + if err := s.resetTokens.DeleteByUserID(user.ID); err != nil { + return err + } + + record := &models.PasswordResetToken{ + UserID: user.ID, + TokenHash: auth.HashToken(plainToken), + ExpiresAt: auth.ResetTokenExpiresAt(s.nowFn()), + } + if err := s.resetTokens.Create(record); err != nil { + return err + } + + return s.sendResetEmail(ctx, user, plainToken) +} + +func (s *AuthService) ResetPassword(token, newPassword string) error { + if strings.TrimSpace(newPassword) == "" { + return ErrInvalidCredentials + } + + hash := auth.HashToken(token) + record, err := s.resetTokens.FindValidByHash(hash, s.nowFn()) + if err != nil { + return err + } + if record == nil { + return ErrInvalidOrExpiredToken + } + + passwordHash, err := auth.HashPassword(newPassword) + if err != nil { + return err + } + + if err := s.users.UpdatePasswordHash(record.UserID, passwordHash); err != nil { + return err + } + + return s.resetTokens.DeleteByID(record.ID) +} + +func (s *AuthService) issueVerifyEmail(ctx context.Context, user *models.User) error { + plainToken, err := auth.NewToken() + if err != nil { + return err + } + if err := s.verifyTokens.DeleteByUserID(user.ID); err != nil { + return err + } + + record := &models.EmailVerificationToken{ + UserID: user.ID, + TokenHash: auth.HashToken(plainToken), + ExpiresAt: auth.VerifyTokenExpiresAt(s.nowFn()), + } + if err := s.verifyTokens.Create(record); err != nil { + return err + } + + verifyURL := strings.TrimRight(s.cfg.BaseURL, "/") + "/verify-email?token=" + url.QueryEscape(plainToken) + htmlBody, textBody, err := s.templateRender.RenderVerifyEmail(mailer.TemplateData{ + AppName: s.cfg.AppName, + BaseURL: s.cfg.BaseURL, + VerifyURL: verifyURL, + UserEmail: user.Email, + }) + if err != nil { + return fmt.Errorf("render verify email: %w", err) + } + + if err := s.mailer.Send(ctx, user.Email, "Verify your email", htmlBody, textBody); err != nil { + return fmt.Errorf("send verify email: %w", err) + } + + return nil +} + +func (s *AuthService) sendResetEmail(ctx context.Context, user *models.User, plainToken string) error { + resetURL := strings.TrimRight(s.cfg.BaseURL, "/") + "/reset-password?token=" + url.QueryEscape(plainToken) + htmlBody, textBody, err := s.templateRender.RenderResetPassword(mailer.TemplateData{ + AppName: s.cfg.AppName, + BaseURL: s.cfg.BaseURL, + ResetURL: resetURL, + UserEmail: user.Email, + }) + if err != nil { + return fmt.Errorf("render reset email: %w", err) + } + + if err := s.mailer.Send(ctx, user.Email, "Reset your password", htmlBody, textBody); err != nil { + return fmt.Errorf("send reset email: %w", err) + } + + return nil +} + +func normalizeEmail(email string) string { + return strings.ToLower(strings.TrimSpace(email)) +} diff --git a/web/templates/layout.html b/web/templates/layout.html index 29ebd1a..54e393f 100644 --- a/web/templates/layout.html +++ b/web/templates/layout.html @@ -1,42 +1,40 @@ -{{- $flashSuccess := index . "FlashSuccess" -}} -{{- $flashError := index . "FlashError" -}} -{{- $nav := index . "NavSection" -}} - {{index . "Title"}} + {{.Title}}
- {{if $flashSuccess}} -
{{$flashSuccess}}
- {{end}} - {{if $flashError}} -
{{$flashError}}
- {{end}} - -
- {{index . "Content"}} -
+ {{template "_flash.html" .}} +
+ {{template "content" .}} +
diff --git a/web/templates/public/_flash.html b/web/templates/public/_flash.html new file mode 100644 index 0000000..adaa190 --- /dev/null +++ b/web/templates/public/_flash.html @@ -0,0 +1,6 @@ +{{if .FlashSuccess}} +
{{.FlashSuccess}}
+{{end}} +{{if .FlashError}} +
{{.FlashError}}
+{{end}} diff --git a/web/templates/public/forgot_password.html b/web/templates/public/forgot_password.html new file mode 100644 index 0000000..7f5ef8d --- /dev/null +++ b/web/templates/public/forgot_password.html @@ -0,0 +1,9 @@ +{{define "content"}} +

Password dimenticata

+

Inserisci la tua email. Se l'account esiste e risulta verificato, invieremo un link di reset.

+
+ + + +
+{{end}} diff --git a/web/templates/public/home.html b/web/templates/public/home.html new file mode 100644 index 0000000..d6703f7 --- /dev/null +++ b/web/templates/public/home.html @@ -0,0 +1,9 @@ +{{define "content"}} +

Trustcontact

+

Boilerplate GoFiber + HTMX + Svelte CE + GORM.

+
+ Crea account + Accedi + Password dimenticata +
+{{end}} diff --git a/web/templates/public/login.html b/web/templates/public/login.html new file mode 100644 index 0000000..b212099 --- /dev/null +++ b/web/templates/public/login.html @@ -0,0 +1,11 @@ +{{define "content"}} +

Login

+
+ + + + + +
+

Non hai un account? Registrati

+{{end}} diff --git a/web/templates/public/reset_password.html b/web/templates/public/reset_password.html new file mode 100644 index 0000000..4be962c --- /dev/null +++ b/web/templates/public/reset_password.html @@ -0,0 +1,12 @@ +{{define "content"}} +

Reset password

+{{if .Token}} +
+ + + +
+{{else}} +

Token mancante o non valido.

+{{end}} +{{end}} diff --git a/web/templates/public/signup.html b/web/templates/public/signup.html new file mode 100644 index 0000000..db401af --- /dev/null +++ b/web/templates/public/signup.html @@ -0,0 +1,11 @@ +{{define "content"}} +

Sign up

+
+ + + + + +
+

Hai già un account? Accedi

+{{end}} diff --git a/web/templates/public/verify_notice.html b/web/templates/public/verify_notice.html new file mode 100644 index 0000000..67cc80a --- /dev/null +++ b/web/templates/public/verify_notice.html @@ -0,0 +1,6 @@ +{{define "content"}} +

Verifica email

+

Controlla la casella di posta e apri il link di verifica ricevuto.

+

Se il link è scaduto, ripeti la registrazione o contatta supporto.

+

Vai al login

+{{end}}