feat: add navigation links management and user registration
Features: - Custom navigation links management in admin settings - Multi-language support for navigation links (Chinese/English) - Control link behavior (open in new window) - Sort order and enable/disable toggle for nav links - User registration system with validation - Admin can enable/disable user registration - Registration form with username, display name, email, password - Link to registration from login page Navigation Links: - Admin interface to add/edit/delete custom nav links - Support for both internal and external URLs - Display links in header navigation bar - Respects language preference User Registration: - Username validation (3-32 characters, alphanumeric + underscore/dash) - Password validation (minimum 6 characters) - Password confirmation matching - Optional display name and email fields - Can be toggled on/off by admin in site settings Templates: - Added navigation links settings page - Added user registration page - Updated login page with registration link - Updated base layout to render custom nav links Documentation: - NAV_LINKS_FEATURE.md - Navigation links feature documentation - REGISTRATION_FEATURE.md - User registration documentation - IMPLEMENTATION_SUMMARY.md - Overall implementation summary Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -19,6 +20,11 @@ func LoginPage() gin.HandlerFunc {
|
||||
if c.Query("error") == "1" {
|
||||
data["Error"] = tr["login_error"]
|
||||
}
|
||||
// Check if registration is allowed from site settings
|
||||
siteSetting, _ := c.Get("site_setting")
|
||||
if s, ok := siteSetting.(*models.SiteSetting); ok && s != nil {
|
||||
data["AllowRegistration"] = s.AllowRegistration
|
||||
}
|
||||
c.HTML(http.StatusOK, "login", data)
|
||||
}
|
||||
}
|
||||
@@ -73,3 +79,106 @@ func Logout() gin.HandlerFunc {
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterPage renders the registration form (only when registration is enabled).
|
||||
func RegisterPage(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Check if registration is allowed
|
||||
var s models.SiteSetting
|
||||
if err := db.First(&s, 1).Error; err != nil || !s.AllowRegistration {
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
return
|
||||
}
|
||||
|
||||
tr := getTr(c)
|
||||
data := DefaultData(c)
|
||||
data["Title"] = tr["page_register"]
|
||||
|
||||
if errMsg := c.Query("error"); errMsg != "" {
|
||||
data["Error"] = tr[errMsg]
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "register", data)
|
||||
}
|
||||
}
|
||||
|
||||
// Register processes the registration form submission.
|
||||
func Register(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Check if registration is allowed
|
||||
var s models.SiteSetting
|
||||
if err := db.First(&s, 1).Error; err != nil || !s.AllowRegistration {
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
return
|
||||
}
|
||||
|
||||
username := strings.TrimSpace(c.PostForm("username"))
|
||||
password := c.PostForm("password")
|
||||
confirmPassword := c.PostForm("confirm_password")
|
||||
email := strings.TrimSpace(c.PostForm("email"))
|
||||
displayName := strings.TrimSpace(c.PostForm("display_name"))
|
||||
|
||||
// Validate inputs
|
||||
if username == "" || password == "" {
|
||||
c.Redirect(http.StatusFound, "/register?error=register_required")
|
||||
return
|
||||
}
|
||||
|
||||
if len(username) < 3 || len(username) > 32 {
|
||||
c.Redirect(http.StatusFound, "/register?error=register_username_length")
|
||||
return
|
||||
}
|
||||
|
||||
if len(password) < 6 {
|
||||
c.Redirect(http.StatusFound, "/register?error=register_password_length")
|
||||
return
|
||||
}
|
||||
|
||||
if password != confirmPassword {
|
||||
c.Redirect(http.StatusFound, "/register?error=register_password_mismatch")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if username already exists
|
||||
var existingUser models.User
|
||||
if err := db.Where("username = ?", username).First(&existingUser).Error; err == nil {
|
||||
c.Redirect(http.StatusFound, "/register?error=user_username_exists")
|
||||
return
|
||||
}
|
||||
|
||||
// Create new user
|
||||
user := models.User{
|
||||
Username: username,
|
||||
Email: email,
|
||||
DisplayName: displayName,
|
||||
Role: models.RoleAuthor,
|
||||
Status: models.StatusNormal,
|
||||
}
|
||||
|
||||
if displayName == "" {
|
||||
user.DisplayName = username
|
||||
}
|
||||
|
||||
if err := user.SetPassword(password); err != nil {
|
||||
c.Redirect(http.StatusFound, "/register?error=register_error")
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Create(&user).Error; err != nil {
|
||||
c.Redirect(http.StatusFound, "/register?error=register_error")
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-login after successful registration
|
||||
session := sessions.Default(c)
|
||||
session.Set("user_id", user.ID)
|
||||
session.Set("username", user.Username)
|
||||
if err := session.Save(); err != nil {
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect to home page
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ func DefaultData(c *gin.Context) gin.H {
|
||||
siteHomeWelcome, _ := c.Get("site_home_welcome")
|
||||
siteHomeSubtitle, _ := c.Get("site_home_subtitle")
|
||||
siteFooterText, _ := c.Get("site_footer_text")
|
||||
navLinks, _ := c.Get("nav_links")
|
||||
|
||||
return gin.H{
|
||||
"Tr": tr,
|
||||
@@ -46,6 +47,7 @@ func DefaultData(c *gin.Context) gin.H {
|
||||
"SiteHomeWelcome": siteHomeWelcome,
|
||||
"SiteHomeSubtitle": siteHomeSubtitle,
|
||||
"SiteFooterText": siteFooterText,
|
||||
"NavLinks": navLinks,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ func SiteSettingsSave(db *gorm.DB, storagePath string) gin.HandlerFunc {
|
||||
s.HomeSubtitleEn = strings.TrimSpace(c.PostForm("home_subtitle_en"))
|
||||
s.FooterTextZh = strings.TrimSpace(c.PostForm("footer_text_zh"))
|
||||
s.FooterTextEn = strings.TrimSpace(c.PostForm("footer_text_en"))
|
||||
s.AllowRegistration = c.PostForm("allow_registration") == "1"
|
||||
s.UpdatedBy = userIDFromSession(c)
|
||||
|
||||
// Favicon upload (optional). A favicon_url form field takes precedence over an
|
||||
@@ -427,3 +428,104 @@ func CommentSettingsSave(db *gorm.DB) gin.HandlerFunc {
|
||||
c.Redirect(http.StatusFound, "/admin/settings/comments?saved=1")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- Navigation Links settings ----------------
|
||||
|
||||
// NavLinksSettingsPage renders the navigation links management page.
|
||||
func NavLinksSettingsPage(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tr := getTr(c)
|
||||
var links []models.NavLink
|
||||
db.Order("sort asc, id asc").Find(&links)
|
||||
|
||||
data := DefaultData(c)
|
||||
data["Title"] = tr["settings_navlinks_title"]
|
||||
data["NavLinks"] = links
|
||||
if msg := c.Query("saved"); msg == "1" {
|
||||
data["Success"] = tr["settings_saved"]
|
||||
}
|
||||
c.HTML(http.StatusOK, "settings_navlinks", data)
|
||||
}
|
||||
}
|
||||
|
||||
// NavLinksSettingsSave dispatches navigation link actions.
|
||||
func NavLinksSettingsSave(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
switch c.PostForm("action") {
|
||||
case "add":
|
||||
addNavLink(db, c)
|
||||
case "toggle":
|
||||
toggleNavLink(db, c)
|
||||
case "edit":
|
||||
editNavLink(db, c)
|
||||
case "delete":
|
||||
deleteNavLink(db, c)
|
||||
}
|
||||
models.RefreshConfigCache(db)
|
||||
c.Redirect(http.StatusFound, "/admin/settings/navlinks?saved=1")
|
||||
}
|
||||
}
|
||||
|
||||
func addNavLink(db *gorm.DB, c *gin.Context) {
|
||||
titleZh := strings.TrimSpace(c.PostForm("title_zh"))
|
||||
titleEn := strings.TrimSpace(c.PostForm("title_en"))
|
||||
url := strings.TrimSpace(c.PostForm("url"))
|
||||
|
||||
if url == "" || (titleZh == "" && titleEn == "") {
|
||||
return
|
||||
}
|
||||
|
||||
sort, _ := strconv.Atoi(c.PostForm("sort"))
|
||||
|
||||
link := models.NavLink{
|
||||
TitleZh: titleZh,
|
||||
TitleEn: titleEn,
|
||||
URL: url,
|
||||
OpenNew: c.PostForm("open_new") == "1",
|
||||
Enabled: c.PostForm("enabled") != "0",
|
||||
Sort: sort,
|
||||
UpdatedBy: userIDFromSession(c),
|
||||
}
|
||||
db.Create(&link)
|
||||
}
|
||||
|
||||
func toggleNavLink(db *gorm.DB, c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.PostForm("id"))
|
||||
var link models.NavLink
|
||||
if db.First(&link, id).Error != nil {
|
||||
return
|
||||
}
|
||||
link.Enabled = !link.Enabled
|
||||
link.UpdatedBy = userIDFromSession(c)
|
||||
db.Save(&link)
|
||||
}
|
||||
|
||||
func editNavLink(db *gorm.DB, c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.PostForm("id"))
|
||||
var link models.NavLink
|
||||
if db.First(&link, id).Error != nil {
|
||||
return
|
||||
}
|
||||
|
||||
titleZh := strings.TrimSpace(c.PostForm("title_zh"))
|
||||
titleEn := strings.TrimSpace(c.PostForm("title_en"))
|
||||
url := strings.TrimSpace(c.PostForm("url"))
|
||||
|
||||
if url == "" || (titleZh == "" && titleEn == "") {
|
||||
return
|
||||
}
|
||||
|
||||
link.TitleZh = titleZh
|
||||
link.TitleEn = titleEn
|
||||
link.URL = url
|
||||
link.OpenNew = c.PostForm("open_new") == "1"
|
||||
link.Sort, _ = strconv.Atoi(c.PostForm("sort"))
|
||||
link.UpdatedBy = userIDFromSession(c)
|
||||
db.Save(&link)
|
||||
}
|
||||
|
||||
func deleteNavLink(db *gorm.DB, c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.PostForm("id"))
|
||||
db.Delete(&models.NavLink{}, id)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user