feat: initial blog engine with multi-language, profile, and role system
- Gin + GORM + Tailwind CSS blog engine - OS-aware config (Linux /etc/blog_go/, Windows ./win/etc/blog_go/) - SQLite by default, MySQL support via config - Auto-migrate database + seed admin user on first run - Session-based auth (login/logout) - i18n: Chinese/English with auto-detect + manual switch - Avatar dropdown nav with click-to-toggle menu - Profile page: avatar upload, edit info, change password - Role system: admin (full access) and author (profile only) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AdminDashboard renders the protected admin dashboard.
|
||||
func AdminDashboard() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tr := getTr(c)
|
||||
username, _ := c.Get("username")
|
||||
data := DefaultData(c)
|
||||
data["Title"] = tr["dash_page_title"]
|
||||
data["Username"] = username
|
||||
c.HTML(http.StatusOK, "dashboard", data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"go_blog/models"
|
||||
)
|
||||
|
||||
// LoginPage renders the login form.
|
||||
func LoginPage() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tr := getTr(c)
|
||||
data := DefaultData(c)
|
||||
data["Title"] = tr["page_login"]
|
||||
if c.Query("error") == "1" {
|
||||
data["Error"] = tr["login_error"]
|
||||
}
|
||||
c.HTML(http.StatusOK, "login", data)
|
||||
}
|
||||
}
|
||||
|
||||
// Login processes the login form submission.
|
||||
func Login(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
username := c.PostForm("username")
|
||||
password := c.PostForm("password")
|
||||
|
||||
var user models.User
|
||||
if err := db.Where("username = ?", username).First(&user).Error; err != nil {
|
||||
c.Redirect(http.StatusFound, "/login?error=1")
|
||||
return
|
||||
}
|
||||
|
||||
if !user.CheckPassword(password) {
|
||||
c.Redirect(http.StatusFound, "/login?error=1")
|
||||
return
|
||||
}
|
||||
|
||||
// Create session.
|
||||
session := sessions.Default(c)
|
||||
session.Set("user_id", user.ID)
|
||||
session.Set("username", user.Username)
|
||||
if err := session.Save(); err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to save session")
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/admin")
|
||||
}
|
||||
}
|
||||
|
||||
// Logout clears the session and redirects home.
|
||||
func Logout() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
session.Clear()
|
||||
session.Save()
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// DefaultData builds a base gin.H map populated with values set by the
|
||||
// SetUserContext middleware (translations, language, auth state). Handlers
|
||||
// add page-specific fields on top and pass it to c.HTML().
|
||||
func DefaultData(c *gin.Context) gin.H {
|
||||
tr, _ := c.Get("tr")
|
||||
isLoggedIn, _ := c.Get("is_logged_in")
|
||||
username, _ := c.Get("username")
|
||||
lang, _ := c.Get("lang")
|
||||
switchLang, _ := c.Get("switch_lang")
|
||||
avatar, _ := c.Get("avatar")
|
||||
displayName, _ := c.Get("display_name")
|
||||
role, _ := c.Get("role")
|
||||
|
||||
return gin.H{
|
||||
"Tr": tr,
|
||||
"IsLoggedIn": isLoggedIn,
|
||||
"Username": username,
|
||||
"Lang": lang,
|
||||
"SwitchLang": switchLang,
|
||||
"Avatar": avatar,
|
||||
"DisplayName": displayName,
|
||||
"Role": role,
|
||||
}
|
||||
}
|
||||
|
||||
// getTr is a convenience helper that returns the translation map from the
|
||||
// Gin context, falling back to an empty map if not set.
|
||||
func getTr(c *gin.Context) map[string]string {
|
||||
tr, ok := c.Get("tr")
|
||||
if !ok {
|
||||
return map[string]string{}
|
||||
}
|
||||
m, ok := tr.(map[string]string)
|
||||
if !ok {
|
||||
return map[string]string{}
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// HomePage renders the public home page.
|
||||
func HomePage() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tr := getTr(c)
|
||||
data := DefaultData(c)
|
||||
data["Title"] = tr["page_home"]
|
||||
c.HTML(http.StatusOK, "home", data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"go_blog/models"
|
||||
)
|
||||
|
||||
// ProfilePage renders the profile edit page.
|
||||
func ProfilePage(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tr := getTr(c)
|
||||
session := sessions.Default(c)
|
||||
userID := session.Get("user_id")
|
||||
|
||||
var user models.User
|
||||
if err := db.First(&user, userID).Error; err != nil {
|
||||
c.String(http.StatusInternalServerError, "User not found")
|
||||
return
|
||||
}
|
||||
|
||||
data := DefaultData(c)
|
||||
data["Title"] = tr["profile_title"]
|
||||
data["Profile"] = user
|
||||
|
||||
// Flash messages (success / error).
|
||||
if msg := c.Query("saved"); msg == "1" {
|
||||
data["Success"] = tr["profile_saved"]
|
||||
}
|
||||
if msg := c.Query("error"); msg == "pw" {
|
||||
data["Error"] = tr["profile_wrong_password"]
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "profile", data)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateProfile processes the profile edit form (multipart).
|
||||
func UpdateProfile(db *gorm.DB, storagePath string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
userID := session.Get("user_id")
|
||||
|
||||
var user models.User
|
||||
if err := db.First(&user, userID).Error; err != nil {
|
||||
c.Redirect(http.StatusFound, "/profile")
|
||||
return
|
||||
}
|
||||
|
||||
// --- Text fields ---
|
||||
if v := c.PostForm("display_name"); v != "" {
|
||||
user.DisplayName = v
|
||||
}
|
||||
if v := c.PostForm("gender"); v != "" {
|
||||
user.Gender = v
|
||||
}
|
||||
if v := c.PostForm("email"); v != "" {
|
||||
user.Email = v
|
||||
}
|
||||
if v := c.PostForm("birthday"); v != "" {
|
||||
if t, err := time.Parse("2006-01-02", v); err == nil {
|
||||
user.Birthday = &t
|
||||
}
|
||||
}
|
||||
|
||||
// --- Avatar upload ---
|
||||
file, header, err := c.Request.FormFile("avatar")
|
||||
if err == nil {
|
||||
defer file.Close()
|
||||
|
||||
// Determine file extension.
|
||||
ext := strings.ToLower(filepath.Ext(header.Filename))
|
||||
if ext == "" || (ext != ".jpg" && ext != ".jpeg" && ext != ".png" && ext != ".gif" && ext != ".webp") {
|
||||
ext = ".jpg"
|
||||
}
|
||||
|
||||
// Save under storagePath/avatars/.
|
||||
avatarDir := filepath.Join(storagePath, "avatars")
|
||||
if err := os.MkdirAll(avatarDir, 0755); err != nil {
|
||||
c.Redirect(http.StatusFound, "/profile")
|
||||
return
|
||||
}
|
||||
|
||||
// Use user ID as filename base to avoid duplicates.
|
||||
savedName := fmt.Sprintf("%d%s", user.ID, ext)
|
||||
savedPath := filepath.Join(avatarDir, savedName)
|
||||
|
||||
dst, err := os.Create(savedPath)
|
||||
if err != nil {
|
||||
c.Redirect(http.StatusFound, "/profile")
|
||||
return
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
if _, err := io.Copy(dst, file); err != nil {
|
||||
c.Redirect(http.StatusFound, "/profile")
|
||||
return
|
||||
}
|
||||
|
||||
user.Avatar = savedName
|
||||
// Update session display immediately.
|
||||
session.Set("avatar", savedName)
|
||||
}
|
||||
|
||||
// --- Password change ---
|
||||
currentPass := c.PostForm("current_password")
|
||||
newPass := c.PostForm("new_password")
|
||||
if currentPass != "" && newPass != "" {
|
||||
if !user.CheckPassword(currentPass) {
|
||||
session.Save()
|
||||
c.Redirect(http.StatusFound, "/profile?error=pw")
|
||||
return
|
||||
}
|
||||
if err := user.SetPassword(newPass); err != nil {
|
||||
session.Save()
|
||||
c.Redirect(http.StatusFound, "/profile")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Save user record.
|
||||
if err := db.Save(&user).Error; err != nil {
|
||||
session.Save()
|
||||
c.Redirect(http.StatusFound, "/profile")
|
||||
return
|
||||
}
|
||||
|
||||
// Update display name in session.
|
||||
session.Set("display_name", user.DisplayName)
|
||||
session.Save()
|
||||
|
||||
c.Redirect(http.StatusFound, "/profile?saved=1")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user